Search code examples
reactjsreduxreact-hooksuse-effectusecallback

React hook useCallback with get API call inside useEffect not behaving correctly


I want to achieve the following.

Action from redux is imported in this component. It fetches data from firebase, which I use as a database. Data is displayed inside this component.

My problem is that data is fetched every time component is mounted, which is not an ideal scenario.

I would like to prevent that behavior.

Ideal scenario is that data should be fetched only first time component is rendered and no more afterwards. Of course it should also fetch when data is changed otherwise no. I used useCallback, but certainly there is a mistake in the code below. Token and Id are passed when user is authenticated.

import { fetchFavs } from "../../store/actions";
import { useSelector, useDispatch } from "react-redux";


const token = useSelector((state) => state.auth.token);
  const id = useSelector((state) => state.auth.userId);
  const fetchedFavs = useSelector((state) => state.saveFavs.fetchedFavs);

  const dispatch = useDispatch();

const fetchFavsOptimized = useCallback(() => {
    dispatch(fetchFavs(token, id));
    console.log("rendered");
  }, [token, id, dispatch]);

  useEffect(() => {
    if (token) {
      fetchFavsOptimized();
    }
  }, [fetchFavsOptimized, token]);

Solution

  • There are two ways to handle this, one explicit and one implicit.

    The explicit way would be to add a new boolean item in your redux store and check against that every time you load your component to see if you should load the data from the db or not.

    So your redux store may have a new key/value: dataLoaded: false; In your component now you need to load this from your redux store and check against it in your use effect:

    const dataLoaded = useSelector((state) => state.dataLoaded);
    
    useEffect(() => {
        if (token && !dataLoaded) {
          fetchFavsOptimized();
        }
      }, [fetchFavsOptimized, token, dataLoaded]);
    

    Don't forget in your fetchFavsOptimized function to update the dataLoaded to true after the loading of the data from the db is complete. This has the upside that every time you want to load the db from the db again you simply need to set dataLoaded to false.

    The implicit way is to check the data length and if that is 0 then execute the load. Assuming that you store data in your redux store in this form: data: [] and your data is an array (you could do the same thing by initiating data as null and check if data !== null) your component would look like this:

    const data = useSelector((state) => state.data);
    
    useEffect(() => {
        if (token && data.length === 0) {
          fetchFavsOptimized();
        }
      }, [fetchFavsOptimized, token, data]);
    

    Keep in mind that I wouldn't do it like that because if you don't have any data you may end up with an infinite loop, but it's a good technique to use in other cases.