Search code examples
reactjstypescriptnext.js

Reusable component for loading data


I'm Building a react/nextjs/redux app I have components each will call an individual backend API

The issue I'm having is making the component that loads in the data more reusable there is some repetitiveness that I am hoping to get rid of

Component 1:

import { StateLoading } from "@/shared/constants/loading";

import { useSelectorEffect } from "@/hooks/useSelector";
import { getGames, selectGames, selectStatus } from "@/store/games/gamesSlice";
import { Game } from "@/shared/interfaces/game";

const GamesDisplay = () => {
  const games = useAppSelector(selectGames) as Game[];
  const isClientLoaded = useSelectorEffect(games, getGames); //custom hook below
  const isLoading = useAppSelector(selectStatus) === StateLoading.LOADING;

  if (isLoading) {
    return <div>Loading....</div>;
  }
  return (
    <>
      <div>Games Display</div>
      {isClientLoaded && (
        <ul>
          {games?.map((comic: Game) => (
            <li key={comic.id}>{comic.title}</li>
          ))}
        </ul>
      )}
    </>
  );
};
export default GamesDisplay;

Component 2:

import { Comic } from "@/shared/interfaces/comic";
import { StateLoading } from "@/shared/constants/loading";

import { useSelectorEffect } from "@/hooks/useSelector";

const ComicsDisplay = () => {
  const comics = useAppSelector(selectComicsArray) as Comic[];
  const isClientLoaded = useSelectorEffect(comics, getComics); //custom hook below
  const isLoading = useAppSelector(selectStatus) === StateLoading.LOADING;

  if (isLoading) {
    return <div>Loading....</div>;
  }
  return (
    <>
      <div>MusicDisplay</div>
      {isClientLoaded && (
        <ul>
          {comics?.map((comic: Comic) => (
            <li key={comic.id}>{comic.title}</li>
          ))}
        </ul>
      )}
    </>
  );
};
export default ComicsDisplay;

As you can see they're quite similar

custom hook:

import { useEffect, useState } from "react";

import { useAppDispatch } from "./store.hooks";

export const useSelectorEffect = (slice: any, dispatchAction: any) => {
  const dispatch = useAppDispatch();
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    if (slice === undefined) {
      dispatch(dispatchAction());
    }

    setIsClient(true);
  }, [slice, dispatchAction, dispatch, isClient]);

  return isClient;
};

Solution

  • You'll need to declare an interface for Comic and Game.

    import React from "react";
    import { StateLoading } from "@/shared/constants/loading";
    import { useSelectorEffect } from "@/hooks/useSelector";
    import { useAppSelector } from "@/store/store.hooks";
    
    interface ReusableDisplayProps<T> {
      itemsSelector: (state: any) => T[];
      statusSelector: (state: any) => string;
      fetchAction: () => void;
      itemRenderer: (item: T) => React.ReactNode;
      title: string;
    }
    
    const ReusableDisplay = <T extends { id: string }>({
      itemsSelector,
      statusSelector,
      fetchAction,
      itemRenderer,
      title,
    }: ReusableDisplayProps<T>) => {
      const items = useAppSelector(itemsSelector);
      const isClientLoaded = useSelectorEffect(items, fetchAction);
      const isLoading = useAppSelector(statusSelector) === StateLoading.LOADING;
    
      if (isLoading) {
        return <div>Loading....</div>;
      }
    
      return (
        <>
          <div>{title}</div>
          {isClientLoaded && (
            <ul>
              {items?.map((item) => (
                <li key={item.id}>{itemRenderer(item)}</li>
              ))}
            </ul>
          )}
        </>
      );
    };
    
    export default ReusableDisplay;
    
    

    Game display

    const GamesDisplay = () => {
      return (
        <ReusableDisplay<Game>
          itemsSelector={selectGames}
          statusSelector={selectStatus}
          fetchAction={getGames}
          itemRenderer={(game) => game.title}
          title="Games Display"
        />
      );
    };
    
    export default GamesDisplay;
    

    Component 2:

    const ComicsDisplay = () => {
      return (
        <ReusableDisplay<Comic>
          itemsSelector={selectComicsArray}
          statusSelector={selectStatus}
          fetchAction={getComics}
          itemRenderer={(comic) => comic.title}
          title="Comics Display"
        />
      );
    };
    
    export default ComicsDisplay;