redux-saga / saga-query

Data synchronization using a middleware system for front-end apps
64 stars 4 forks source link

:sparkles: Provide better granularity on loading state on a per item basis #12

Closed neurosnap closed 2 years ago

neurosnap commented 2 years ago

This is a pretty fundamental change to how users use our react hooks. We want to be able to support generic loaders for an endpoint but also to be able to create loaders based on the payload signature when being called. This will support the ability to have loaders for individual items as opposed to an entire endpoint.

For example:

const api = createApi();
const fetchUser = api.get('/user/:id');

Previously, we would have a loader that would only track the loading status of the entire endpoint. So if a user dispatched the same action but with two different ids, the original loader would get overwritten by the next action being dispatched.

dispatch(fetchUser({ id: '1' }));
dispatch(fetchUser({ id: '2' })); // this event would clobber the original loader

In order to address this problem I've decided that we could dispatch two loaders simultaneously for every API endpoint dispatch. The actions are batched so there should be no performance issues and it provides the flexibility to have a loader globally for the endpoint or specifically for various payload signatures (like different item ids). We accomplish this goal by have a special key property on all of our actions which is created automatically by createPipe. As a result, every action created by saga-query has a property at action.payload.key which is a base64 string of the name of the api endpoint as well as the payload of the action when dispatched. This is what we use to create the loaders.

The only caveat to this structure is that now when users want to select the loader based on the action key, they need to create the action object and then pass it into the react hooks or loader selector. As such, I've re-implemented the react hooks to accommodate such a change.

Both of the following examples work, choosing which one primarily depends if you want the api endpoint loader or the individual resource loader.

Pass in action creator

const fetchUser = api.get('/users/:id', function*(ctx, next) {
  yield next();
  // save user
});

const Page = () => {
  const { data, isLoading } = useApi(fetchUser);
  // or
  const loader = useLoader(fetchUser);
  // ...
}

Pass in action

const Page = () => {
  const { data, isLoading } = useApi(fetchUser({ id: '1' }));
  // or
  const loader = useLoader(fetchUser({ id: '1' }));
  // ...
}

Other notable changes

Addresses #11