2

I have a custom hook that is responsible for fetching some user-related data from my backend on app initialization and storing it in global state that is required by multiple components. The data-fetching is triggered from a useEffect inside of the hook, and since multiple components (that are rendered in the same view) are all calling the hook to access the data, said useEffect is firing multiple times, hence the API is called as many times as the hook is called.

Here is a simplified version of what's going on inside of the custom hook:

const useMyCustomHook = () => {
  const [user] = useAuthState(auth); // hook for accessing firebase user
  const [data, setData] = useRecoilState([]); // using Recoil state-management lib for global state

  useEffect(() => {
    if (!user || data.length) return;

    const fetchData = async () => {
      const apiData = await APICall(someAPIURL);
      setData(apiData);
    };
  }, [user, data]);

  return { data };
};

This data is access in several components via:

const { data } = useMyCustomHook();

So basically the if statement inside of the useEffect protects against API calls if the data is in state, however, since on initialization, the useEffect is firing multiple times (since the components calling it are on the screen at the same time), each triggering an async call that hasn't finished before the other components trigger the same effect, therefore the API is called multiple times since state has not yet been populated by the preceding call.

What would be a way to avoid this? Is there a way to let the other components using the hook know that the initial API fetch is 'inProgress' or something?

Any advice is appreciated. Thank you very much.

Shadee Merhi
  • 194
  • 2
  • 13
  • 1
    Try creating a state that tracks if the API has been called and if so the if statement will trigger, if you want an example let me know and I will write an answer. The behavior probably happens because the data hasn't got its value yet, and the useEffect trigger on data change – omercotkd Mar 23 '22 at 20:32
  • @omercotkd Thank you for the comment, that's a great idea. I will give it a try! – Shadee Merhi Mar 23 '22 at 21:23
  • @omercotkd Would you actually be able to provide an example? I tried doing something along the lines of what you said but I'm still having the same issue – Shadee Merhi Mar 23 '22 at 22:14
  • Anthony C answer is what I meant, if its not working for you I don't have another idea right now – omercotkd Mar 23 '22 at 22:29
  • See [this Q&A](https://stackoverflow.com/a/71324083/18200347) for an example of `useAsync` – よつば Mar 24 '22 at 03:45

1 Answers1

2

I'd add a state to keep track of fetching status and put a useEffect on said state. Having a diff useEffect on the fetching state would prevent it trigger multiple times when not needed.

const useMyCustomHook = () => {
  const [user] = useAuthState(auth); // hook for accessing firebase user
  const [needFetching, setNeedFetching] = useState(false);
  const [data, setData] = useRecoilState([]); // using Recoil state-management lib for global state

  useEffect(() => {
    if (!user || data.length || needFetching) return;

    setNeedFetching(true);
  }, [user, data]);

  // this only trigger when needFetching state is changed
  useEffect(() => {
    if (!needFetching) return;

    const fetchData = async () => {
      const apiData = await APICall(someAPIURL);
      setData(apiData);
      needFetching(false);
    };
  }, [needFetching]);

  return { data };
};
Anthony C
  • 2,117
  • 2
  • 15
  • 26
  • Thank you for the reply. Since the ```needsFetching``` state is initialized for each hook call and is independent of the other hook calls, won't the second ```useEffect``` still be fired the same amount of times though? Will I need to make the ```needsFetching``` state global so that all other hook calls are using the same value of it and not their own? – Shadee Merhi Mar 23 '22 at 21:49
  • Will I need another piece of state that would track which specific hook render was the one that initialized the call? e.g. if the hook is called 3 times, each call could pass in an 'ID', and in the ```useEffect``` could check if the current render matches the ID that initialized the request? I'm not sure if that makes sense haha – Shadee Merhi Mar 23 '22 at 22:13
  • 1
    This is why we use react-query. What you want is to have multiple components all subscribed to the api fetch so when the data comes back it's available to all of them, and only one call will be made. – Chad S. Mar 23 '22 at 22:58
  • useEffect triggers only when the prop it's listening to get changed. Since `setNeedFetching(true)` is called in the first `useEffect`, the value of `needFetching` remains true in the subsequence call so it doesn't get triggered again, until the APICall is returned. – Anthony C Mar 23 '22 at 23:38
  • If the data is ID specific, you will need to change the `data` to a map and change the needFetching to a list as well. The `data` will have `id` as they key to map to the `id` specific data`, the `needFetching` list will have a list of ids that is getting data from the api. The condition `if (!needFetching) return;` needs to change to check if the id exist in the list – Anthony C Mar 23 '22 at 23:44
  • I see, thank you for the insights, really appreciate it! – Shadee Merhi Mar 24 '22 at 19:50
  • 1
    needFetching is independent on each useMyCustomHook usage, so I don't think it should prevent multiple api call when we use useMyCustomHook in multiple places – Quoc Van Tang May 25 '22 at 13:41