Search code examples
reactjstypescriptreduxreact-reduxredux-toolkit

How to handle empty initial state in redux which is to be populated later


I am having some problem using redux with ts my initial state is {} and later it is populated with keys and values so when I try use that state in my component, its saying that this property does not exist on type {} even though I am sure that this component renders only after the state has been populated. How can I fix this error?

I am trying to use it like this:

const TopSection = () => {
  const { username } = useAppSelector((state) => state.auth);
  const mediaDetails: MediaDetailState = useAppSelector((state) => state.media);
  const { mediaid, mediaType } = mediaDetails;

  const routes = [
    { path: "/", title: "Overview" },
    { path: "watch", title: "Watch" },
    { path: "characters", title: "Characters" },
    { path: "staff", title: "Staff" },
    { path: "stats", title: "Stats" },
    { path: "social", title: "Social" },
  ];

  return (
    <div className="bg-bgSecondary">
      {/* Backdrop image */}
      {mediaDetails.backdrop_path && (
        <div className="h-[25vh] md:h-[50vh] overflow-hidden">
          <img
            src={`${tmdbImgEndPoint}${mediaDetails.backdrop_path}`}
            alt={mediaType === "movie" ? mediaDetails.title : mediaDetails.name}
            className="object-top"
          />
        </div>
      )}
      {/* Poster and overview */}
      <div className="grid grid-cols-12 px-12 md:px-56">
        {/* Poster and buttons */}
        <div className="col-span-12 md:col-span-2 grid grid-cols-3 gap-4 md:block">
          <img
            src={
              mediaDetails.poster_path
                ? `${tmdbImgEndPoint}${mediaDetails.poster_path}`
                : posterPlaceholder
            }
            alt={mediaDetails.title}
            className={`${
              mediaDetails.backdrop_path ? "-mt-28" : "mt-2"
            } mb-4 rounded`}
          />
          {username && <Controls />}
        </div>
        {/* title and overview and links */}
        <div className="col-span-12 md:col-span-9 ms-0 md:ms-4 p-0 md:p-8 flex flex-col justify-between">
          <div className="mb-8 md:mb-0">
            <h1 className="text-3xl font-normal">
              {mediaType == "movie" ? mediaDetails.title : mediaDetails.name}
            </h1>
            {mediaDetails.overview && (
              <p className="text-textLight text-[1.4rem] mt-6 hidden md:block">
                {mediaDetails.overview}
              </p>
            )}
          </div>
          {/* Links */}
          <ul className="flex justify-around text-xl" id="pagenav">
            {routes.map((route) => (
              <Link
                className="hover:text-actionPrimary"
                to={`/${mediaType}/${mediaid}/${
                  route.path === "/" ? "" : route.path
                }`}
                key={route.title}
              >
                {route.title}
              </Link>
            ))}
          </ul>
        </div>
      </div>
    </div>
  );
};

Redux:

export type ExtraMediaDetails = { mediaType: mediaTypeType; mediaid: number };
export type MediaDetailState =
  | (MovieDetail & ExtraMediaDetails)
  | (TvDetail & ExtraMediaDetails)
  | {};

let initialState: MediaDetailState = {};

export const mediaSlice = createSlice({
  name: "media",
  initialState,
  reducers: {
    setDetails: (state, action: PayloadAction<MediaDetailState>) => {
      Object.keys(state).forEach(
        (key) => delete state[key as keyof MediaDetailState]
      );
      for (const key in action.payload) {
        state[key as keyof MediaDetailState] =
          action.payload[key as keyof MediaDetailState];
      }
    },
    removeDetails: (state) => {
      Object.keys(state).forEach(
        (key) => delete state[key as keyof MediaDetailState]
      );
    },
  },
});

Here are rest of the types:

type MediaDetailBase = {
  adult: boolean;
  backdrop_path: string;
  homepage: string;
  id: number;
  original_language: string;
  overview: string;
  popularity: number;
  poster_path: string;
  production_companies: ProductionCompany[];
  production_countries: ProductionCountry[];
  spoken_languages: Language[];
  status: string;
  tagline: string;
  vote_average: number;
  vote_count: number;
};

export type MovieDetail = MediaDetailBase & {
  belongs_to_collection: string;
  budget: number;
  genre_ids: MediaDetailGenre[];
  imdb_id: string;
  original_title: string;
  release_date: string;
  revenue: number;
  runtime: number;
  title: string;
  video: boolean;
};

export type TvDetail = MediaDetailBase & {
  created_by: TvCreator[];
  episode_run_time: number[];
  first_air_date: string;
  genres: MediaDetailGenre[];
  in_production: boolean;
  languages: string[];
  last_air_date: string;
  last_episode_to_air: Episode;
  name: string;
  next_episode_to_air: string;
  networks: Network[];
  number_of_episodes: number;
  number_of_seasons: number;
  origin_country: string[];
  original_name: string;
  seasons: Season[];
  type: string;
};

I was trying to use redux with typescript I initialised my state as {} and later it is to be populated

but ts is showing error that the state might be {} while fetching it from useSelector()

It is the expected behavior. I want to know to setup redux with ts so that these things don't happen


Solution

  • You could make the initial state include mediaType and mediaid as optional. Something like {} & Partial<ExtraMediaDetails>.

    export type ExtraMediaDetails = {
      mediaType: mediaTypeType;
      mediaid: number
    };
    
    export type MediaDetailState =
      | (MovieDetail & ExtraMediaDetails)
      | (TvDetail & ExtraMediaDetails)
      | ({} & Partial<ExtraMediaDetails>);
    

    ExtraMediaDetails is indeed of the type { mediaType: mediaTypeType; mediaid: number }; But mediaid and mediaType are not the only properties that I am trying to extract from the state there are other properties such as title, poster_path which are defined in MovieDetail or TvDetail but not in ExtraMediaDetails

    For this you'll need to test for which type you have at runtime. I often suggest adding a type property based on generics.

    Example:

    type MediaDetailBase<T extends string> = {
      type: T;
      adult: boolean;
      backdrop_path: string;
      homepage: string;
      id: number;
      original_language: string;
      overview: string;
      popularity: number;
      poster_path: string;
      production_companies: ProductionCompany[];
      production_countries: ProductionCountry[];
      spoken_languages: Language[];
      status: string;
      tagline: string;
      vote_average: number;
      vote_count: number;
    };
    

    Then type the MovieDetail and TvDetail types:

    export type MovieDetail = MediaDetailBase<"Movie"> & {
      belongs_to_collection: string;
      budget: number;
      genre_ids: MediaDetailGenre[];
      imdb_id: string;
      original_title: string;
      release_date: string;
      revenue: number;
      runtime: number;
      title: string;
      video: boolean;
    };
    
    export type TvDetail = MediaDetailBase<"TV"> & {
      created_by: TvCreator[];
      episode_run_time: number[];
      first_air_date: string;
      genres: MediaDetailGenre[];
      in_production: boolean;
      languages: string[];
      last_air_date: string;
      last_episode_to_air: Episode;
      name: string;
      next_episode_to_air: string;
      networks: Network[];
      number_of_episodes: number;
      number_of_seasons: number;
      origin_country: string[];
      original_name: string;
      seasons: Season[];
      type: string;
    };
    

    I think MediaDetailState can be simplified just a tad to:

    export type MediaDetailState =
      | ((MovieDetail | TvDetail) & ExtraMediaDetails)
      | {};
    

    And in your UI code where you access the state.media value check the type property and cast to the appropriate type to access the correct runtime properties.

    Example:

    const mediaDetails: MediaDetailState = useAppSelector((state) => state.media);
    
    if ("type" in mediaDetails) {
      switch (mediaDetails.type) {
        case "Movie": {
          // Cast as MovieDetail & ExtraMediaDetails
          const {
             mediaType,
             mediaid,
             // Other MovieDetail properties
          } = mediaDetails as MovieDetail & ExtraMediaDetails;
          break;
        }
        case "TV": {
          // Cast as TvDetail & ExtraMediaDetails
          const {
            mediaType,
            mediaid,
             // Other TvDetail properties
          } = mediaDetails as TvDetail & ExtraMediaDetails;
          break;
        }
      }
    } else {
      // just the empty object
    }