Search code examples
reactjsaxiosfetching-strategy

In React, fetch data conditional on results of an initial fetch


We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:

  • Fetch general info for a specific entity (an NCAA conference, for example)
  • Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.

For this, our code would then look something like this:

import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff

function ComponentThatWantsTeamInfo({ conferenceId }) {
    // use data fetching hook
    const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })

    // once conferenceInfo loads, then load info from all teams in the conference
    if (conferenceInfo && conferenceInfo.teamsArray) {
        const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
    }
}

In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:

  1. Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
  2. useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.

What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:

  1. Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
  2. Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.

Edit

I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:

const useInternalApi = (endpoint, config) => {
    // Set Data-Fetching State
    const [data, setData] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [isError, setIsError] = useState(false);

    // Use in lieu of useEffect
    useDeepCompareEffect(() => {
        // Token/Source should be created before "fetchData"
        let source = axios.CancelToken.source();
        let isMounted = true;

        // Create Function that makes Axios requests
        const fetchData = async () => {
            // Set States + Try To Fetch
            setIsError(false);
            setIsLoading(true);
            try {
                const url = createUrl(endpoint, config);
                const result = await axios.get(url, { cancelToken: source.token });
                if (isMounted) {
                    setData(result.data);
                }
            } catch (error) {
                if (isMounted) {
                    setIsError(true);
                }
            } finally {
                if (isMounted) {
                    setIsLoading(false);
                }
            }
        };

        // Call Function
        fetchData();

        // Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
        return () => {
            isMounted = false; // set to false to prevent state updates / memory leaks
            source.cancel(); // and cancel the http request as well because why not
        };
    }, [endpoint, config]);

    // Return as length-3 array
    return [data, isLoading, isError];
};

Solution

  • In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.

    My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?

    Here is how I'd do something of that sorts.

    
    import `useInternalApi` from '../path-to-hooks/useInternalApi';
    // import React... and other stuff
    
    function ComponentThatDisplaysASpecificTeam(props){
        const teamId = props.teamId;
        const [teamInfo] = useInternalApi('teamInfo', { teamId });
    
        if(! teamInfo){
            return <p>Loading...</p>
        }
    
        return <p>do something with teamInfo...</p>
    
    }
    
    function ComponentThatWantsTeamInfo({ conferenceId }) {
        // use data fetching hook
        const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
    
        if (! conferenceInfo || ! conferenceInfo.teamsArray) {
            return <p>this is either a loading or an error, you probably know better than me.</p>
        }
    
        // Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
        return (
            <div>
                {conferenceInfo.teamIds.map(teamId => ( 
                    <ComponentThatDisplaysASpecificTeam teamId={teamId} />
                ))}
            </div>
        )
    
    }