Next.js Discord

Discord Forum

Use `React.cache` on the client

Unanswered
Tomistoma posted this in #help-forum
Open in Discord
TomistomaOP
I have (client) component that makes a number of fetch request. The first fetch request made is based off a value selected in a drop down, and the pending promise is placed into state and passed to a component that calls use so I can render suspense and error boundaries around it. The response data populates a second dropdown.
Once this data returns, the second dropdown can be selected which will fire off 2 more requests, sequentially. However, the data returned by these 2 requests are NOT used for any UI and instead are used within a context, for a map. This is significant because it means I don't have any way of wrapping them in a suspense or error boundary since there is no component associated with them. I would really like to do that, though, because it's a particularly nice paradigm and I don't want to have a bunch of states floating around holding errors from the API if I could just have a component throw an error that is caught by a boundary. Unfortunately, you can't throw errors in a useEffect so my only option is to move the code into a component.

This is where it becomes a challenge because the component needs to go and make these 2 fetch requests, but it also needs to call use on them so the suspense and error boundaries work but you're not allowed to create a promise in the same component that uses them otherwise you get stuck in a render loop. The solution seems to be to use React.cache on the functions that make the request, but as these are client components that just seems to be a no-op. I've got the choice of making many caches myself (which is tedious and ugly) or somehow splitting more of this up into client-server, but I'm not exactly sure how that will work.

Does anyone know a way of using React.cache on the client?

1 Reply

TomistomaOP
Below is example code (comments are hopefully clearer):
'use client';

const getCatchmentGeometry = async (areaTypeId, catchmentName) => {
  // ...
}
const getSamplingPoints = async (geometry) => {
  // ...
};

const AoiData = ({ areaId, catchmentName }) => {
  // The issue lies here becase promise is created every render. `React.cache` and `useMemo` both fail
  const geometryPromise = getCatchmentGeometry(areaId, catchmentName);
  const geometryData = use(geometryPromise);

  // ...use geometryData to update map...

  const samplingPointsPromise = getSamplingPoints(geometryData);
  const samplingPoints = use(samplingPointsPromise);

  //  don't need this component to be visible, it just holds logic
  return null;
};

export const SearchTab = () => {
  const [areaId, setAreaId] = useState(...); // selected via a dropdown
  const [catchmentNamePromise, setCatchmentNamePromise] = useState(...);
  const [selectedCatchment, setSelectedCatchment] = useState(...); // selected via second dropdown

  return (
    <div>
      <Dropdown
        // set promise when dropdown changes
        onChange={(o) => setCatchmentNamePromise(getCatchmentNames(o))}
      />

      {
        catchmentNamePromise 
        ?
            .... // the second dropdown, wrapped in `Suspense` and `ErrorBoundary`. When this changes, `setSelectedCatchment` is called
        : null
      }

      { /* 
        I want to do it this way so I can wrap it in Suspense and Error boundaries.
        I could do the logic in a `useEffect`, but then I need to state to for the error, etc, and
        it gets messy and goes against the exact features we were given for this purpose (imo)
      */}
      {
        selectedCatchment 
          ? <ErrorBoundary errorComponent={() => <></>}>
              <Suspense fallback={<></>}>
                <AoiData areaId={areaId} catchmentName={catchmentName} />
              </Suspense>
            </ErrorBoundary>
          : null
      }
    </div>
  );
};