How to model data fetching by domain with React and Suspense
Tags: react, discuss
Published: 2020-03-22 19:27:23

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:

```javascript
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.

```javascript
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.

```javascript
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?