A resource state is a value that indicates whether the resource is loading or updating. Since tracking resource states is not trival, it's a good idea to only track the resources that require it. We'll return to this idea in the next chapter.
We can store a resource state externally (in some lookup table) or internally in the resource itself. I prefer to store it internally for several reasons:
To add a resource state, I'm using a initRS(resource)
function. This function adds the internal resource state if
the resource doesn't have one:
export const symbolRS = Symbol('ResourceState');
export const initRS = (resource: any) => {
if (resource && !resource[symbolRS]) {
resource[symbolRS] = new ResourceState();
}
return resource;
};
Instead of storing a value such loading
or updating
in the ResourceState
class, I'm storing a list of sources
from which the final state is derived. This approach makes it possible to construct
a pipeline of resource states. For example, the state of a todo
resource can be derived from the state of
a todolist
resource, that can be derived from a todolists
resource. By using such a pipeline it's easier
to ensure that the final resource state is correct, and not based on stale data.
Here is the source code for the ResourceState
class:
export type ConditionT = () => boolean | undefined;
export type SourceT = {
state: string;
condition: ConditionT;
name: string;
};
export class ResourceState {
@observable name?: string = undefined;
@observable sources: SourceT[] = [];
@action addSource(source: SourceT) { // Implementation omitted for brevity }
@action removeSource(state: string, name: string) { // Implementation omitted for brevity }
@computed get value() {
return this.sources
.filter((source: SourceT) => source.condition())
.map((source: SourceT) => source.state);
}
constructor() { makeObservable(this); }
}
As we can see, the value of a ResourceState
is an array that contains all resource states of all sources for which
source.condition()
returns true.
The updateSources
function is a helper function that is used as follows:
const resources = updateSources(
{ resource: myTodolist },
['loading', () => isQueryLoading(getTodolists), 'getTodolists'],
)
This function does the following:
initRS
on the resource (in this case: myTodolist
) to create a resource state;getTodolists
query is loading.While a resource is loading, we don't have it. Therefore, we cannot access the resource state via the resource instance.
Instead, I use null
to represent a loading resource. When a React component receives a resource, it can be either
null
(which means that the resource is loading), or some object (that has a resource state), or undefined
(which
means that the resource is unavailable, but not in a loading
state).
Here is the code for getting the resource state of a resource:
import { symbolRS } from '/src/resourceStates/ResourceState';
export const loadingResource = null;
export const getState = <StateT = string>(
resource: any
): StateT[] => {
return resource === undefined
? []
: resource === loadingResource
? ['loading']
: resource[symbolRS].value;
};
export const isUpdating = (resource: any) => getState(resource).includes('updating');
export const isLoading = (resource: any) => getState(resource).includes('loading');
It often happens that the resource state is updating
while some promise is being executed. For example,
while we are running deleteTodolists
, the resource state of the affected todolists
resources should
be updating
. The trackPromise
function helps to manage this. While the promise is running, it attaches
a source to the resource state of the affected resources. This source effectively puts these todolists in the
updating
state. When the promise is resolved, the source is removed.
const todolistsToBeDeleted = [todolist1, todolist2];
const promise = trackPromise({
promise: deleteTodoLists.mutateAsync({
todolistIds: getIds(todolistsToBeDeleted),
}),
// Add 'updating' to the resource state of the
// todolists that are being deleted
states: { updating: [todolistsToBeDeleted] },
}).result;
When you store resource states inside the resource objects themselves, there is a potential
problem when fresh data is fetched from the server. If you replace the local resource
with the newly fetched one then the local resource state is lost. This is a problem when
you are locally updating a resource, for example: when the resource is being deleted.
To avoid this, we should move the resource state from the old resource to the new resource.
This is what the graftResourceStatesFromMemo
function does. It's used as follows:
// We're using React.useState to locally store the todolists.
// This is not what I would use in a real application,
// but for illustration purposes it's good enough.
const [todolists, setTodolists] = React.useState<TodolistT[]>([]);
const graftResourceStatesFromMemo = useGraftResourceStatesFromMemo({});
React.useEffect(() => {
getTodolists().then(todolists => {
setTodolists(graftResourceStatesFromMemo({
resources: todolists
});
});
})
When you pass an array of resources to the graftResourceStatesFromMemo
function then it stores a memo
of the resource states. The next time you call graftResourceStatesFromMemo
it will copy the
resource states from the memo to the new resources. This way, the resource states are preserved
when the resources are replaced.
In the next chapter we'll look at how we can observe the state of queries and mutations. This is necessary since resource states usually depend on which queries and mutations are currently running.