DEV Community

Will
Will

Posted on

How to model data fetching by domain with React and Suspense

I've been thinking a lot about React Suspense lately and how to model cache + data fetching in a Suspense-ful way.

A somewhat easy implementation is to cache based on request, e.g. associate some key with a particular request, store the promise in the cache at that key, and check to see if it's settled before throwing a promise. This is what react-cache does as well as SWR and many other data fetching hooks that support experimental suspense.

This gets a little hairier when you want to depend on data by domain, rather than request. Say you have a list of parts that you want fetch and display the current state of:

function getPartsList() {
  // kicks off request to get the part list data
  // and returns a promise of the result
}

function PartsList() {
  // suspends while fetching parts list
  let [partsList] = useFetch('parts-list', getPartsList);
  // if 'parts-list' is settled, renders
  return (
    <div>
      {partsList.map(part => <div key={part.id}>part.name</div>)}
    </div>
  );
}

Then, you want to persist updates to the back-end as the user makes changes to an individual part. The request to update the part only returns the new part data; not the whole list. This is common in REST/CRUD apps.

function updatePart(partId, partData) {
  // kicks off a request to update the part on the server
  // and returns a promise of the result
}

function PartEditor({ part }) {
  let [, updatePart] = useFetch('part', updatePart, part.id);
  let [partName, updateName] = useState(part.name);
  return (
    <div>
      <div>
       Part name:
       <input value={partName} onChange={e => updateName(e.target.value)} />
      </div>
      <button onClick={() => updatePart({ name: partName})}>Update</button>
    </div>
  );
}

Let's assume that both the PartsList and PartEditor are on screen at the same time. How do we communicate to the PartsList that the name has changed, and needs to updated (either by re-fetching or updating in place)?

One "simple" way would be to pass in a function to PartEditor to call on update, to allow the parent to trigger a re-fetch of the part list as well. However, I question whether this scales as more components in your application depend on your parts domain, and need to constantly be notified of all changes by hand.

Another way is, rather than using a caching strategy that caches by request, we instead manage a cache by "domain." E.g. we have a parts domain cache that can be read, updated, and will suspend accordingly.

let partsReducer = {
  readMany(parts, data) {
    return data;
  },
  update(parts, partData) {
    // immutably update parts array with new part data
  }
};

let partsCache = cache.create({
  readMany: getPartsList,
  update: updatePart,
}, partsReducer);

function PartsList() {
  let [partsList] = useCache(partsCache, "readMany");
  // ...
}

function PartEditor({ part }) {
  let [, updatePart] = useCache(partsCache, "update" , part.id);
  // ...
}

This seems rather nice; we have a single point of coordination between all state of our parts domain. Updating the name in the PartEditor will suspend it and the PartsList as the update occurs, and both will be re-rendered with fresh data on settle.

However, we lose granularity here; ideally we wouldn't suspend the parts list, but we would react to the changes in the partsCache when the update happens. If we re-fetch the list (say to get parts that other clients have added), we may want to suspend our editor though!

I'm curious to know what others are thinking and experimenting with in regards to modeling data fetching in their applications. I don't think suspense changes too much, only brings front-and-center the problems that we're struggling with in mapping our components and loading state to the domain entities they depend on server-side. Any suggestions or problems with my line of thinking?

Top comments (0)