5

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.

5
  • 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 Commented Mar 23, 2022 at 20:32
  • @omercotkd Thank you for the comment, that's a great idea. I will give it a try! Commented Mar 23, 2022 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 Commented Mar 23, 2022 at 22:14
  • Anthony C answer is what I meant, if its not working for you I don't have another idea right now Commented Mar 23, 2022 at 22:29
  • See this Q&A for an example of useAsync Commented Mar 24, 2022 at 3:45

3 Answers 3

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 };
};
Sign up to request clarification or add additional context in comments.

7 Comments

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?
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
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.
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.
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
|
0

You can try to lock the request

useEffect(() => {
    navigator.locks.request('lockName', {
        ifAvailable: true, // the lock is only granted if it can be without additional waiting
    }, async (lock) => {
        if (!lock) {
            // didn’t get the lock
            // the Effect is simultaneously called by another component to load the data
            return;
        }

        // CALL YOUR API HERE

        // the lock has been automatically released
    });
}, []);

Comments

0

I'll post here one solution since this is one of top results when googling the question. Hopefully it will be useful to somebody.

If you don't want to use any external libraries, you can create a new component that you will attach at the root of your website. See the <APIProvider> here:

export default function RootLayout({ children, }: { children: React.ReactNode }) {

  return (
    <html> 
      <body className={inter.className}>
          <APIProvider>
              {children}
          </APIProvider>
      </body>
    </html>
  )
}

The APIProvider is the one that will do the API call, and store it. We can then use react's useContext hook to access it:

interface APIContextType {
  resultData: string
}

export const APIContext = createContext<APIContextType | undefined>(undefined)

export function APIProvider({ children }: { children: ReactNode }) {
  const [resultData, setResultData] = useState('')

  useEffect(() => {

    // Use your API ... //

  }, []) // Use user.id instead of user object

  return (
    <APIContext.Provider value={{
      resultData
    }}>
      {children}
    </APIContext.Provider>
  )
}
export const useAPIResults = () => {
  const context = useContext(APIContext)
  if (!context) {
    throw new Error('Hook must be used within a Provider')
  }

  return context
}

Following this implementation the code inside useEffect will be called once and only once. Using the custom hook we can access the results in multiple components.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.