react-querytanstacktanstackreact-query

Nested/conditional chained API calls in React Query?


We need a function that may call an API or the other, and based on the result, call yet another API.

Let me give a simplified example. We're trying to do something like this

function getCarDetails(car: CarSummary): UseQueryResult<CarDetails> {
  if(car.make === 'honda') {
    const hondaResponse = useHondaData(car);

    if(hondaResponse.isError)
      return hondaResponse;
    
    return useCarDetailsData(
      {x: hondaResponse.data?.y},
      {enabled: !!hondaResponse.data}
    );
  }
  else if(car.make === 'tesla') {
    const teslaResponse = useTeslaData(car);

    if(teslaResponse.isError)
      return teslaResponse;
    
    return useCarDetailsData(
      {x: teslaResponse.data?.y},
      {enabled: !!teslaResponse.data}
    );
  }
  else {
    return useCarDetailsData(
      {x: 'generic'},
      {enabled: true}
    );
  }
}

The above code would be ideal, if only hooks can be called from inside ifs.

But as hooks can only be called from the top level, what is the best way to achieve the above (i.e. nested/conditional chained API calls using React Query)?


Solution

  • By adding an enabled flag to useHondaData and useTeslaData we can do like below.

    Here we always call the hooks, but disable the ones that are not neeeded. Before return we check if the first call produced an error, in that case we return that first response, otherwise we return the response from useCarDetails.

    function getCarDetails(car: CarSummary): UseQueryResult<CarDetails> {
      // always call the hooks but disable the ones not needed with an `enabled` flag
      const hondaResponse = useHondaData(car, { enabled: car.make === 'honda' });
      const teslaResponse = useTeslaData(car, { enabled: car.make === 'tesla' });
    
      // ignore the types here
      let response: ReturnType<typeof useHondaData> | ReturnType<typeof useTeslaData> | undefined 
      let x: NonNullable<ReturnType<typeof useHondaData>['data']>['y'] | NonNullable<ReturnType<typeof useTeslaData>['data']>['y'] | undefined 
      if (car.make === 'honda' || car.make === 'tesla') {
        // save the appropriate response for later use
        response = car.make === 'honda' ? hondaResponse : teslaResponse
        x = response.data?.y
      } else {
        x = 'generic'
      }
     
      // always call this hook
      const details = useCarDetailsData(
        { x },
        // disable it if we don't have the data we need (x), or if we have an error
        { enabled: !!x && !response?.isError }
      );
    
      // if we have an error, return the first response, otherwise return the details
      return response.isError ? response : details
    }
    

    Note that the approach as described in your question would have getCarDetails return the output of useCarDetails while useHondaData or useTeslaData are loading, but when one of those fails, it would return the output of useHondaData or useTeslaData. This might cause confusion and issues. It would be better if we returned a custom object from getCarDetails.

    E.g. (depending on the needs)

    return {
      data: details.data,
      error: response.error ?? details.error,
      isLoading: response.isLoading || details.isLoading
    }
    

    Also note that since getCarDetails uses hooks, it is itself a hook and it's name should start with use.