I'm using useQuery
function from @tanstack/query
to fetch data from the server. This function
has the benefit of caching the state, so that we can call useQuery
on any render without
overfetching.
When the fetched data becomes available then the React component that called
useQuery
will be re-rendered. However, in my application, it's not only React components that need
to respond to changes in the query status. For example, the query status should also impact
the resource state of the loaded resources (see the call to updateSources
in the previous chapter).
My application uses MobX for this type of reactive behaviour. Therefore, I wrap the result of
useQuery
in an ObservableQuery
object that uses MobX:
export type QueryDataT = ObjT | undefined;
export class ObservableQuery {
@observable data: QueryDataT = undefined;
@observable status: string = 'idle';
@action clear = () => {
this.data = undefined;
this.status = 'idle';
};
constructor() { makeObservable(this); }
}
export const isQueryLoading = (query: ObservableQuery) => {
return query.status === 'loading' && !query.data;
};
export const isQueryUpdating = (query: ObservableQuery) => {
return query.status === 'loading' && !!query.data;
};
export interface TanstackQuery {
data: QueryDataT;
status: string;
isFetching: boolean;
}
export const useObservableQuery = (query: TanstackQuery) => {
const observableQuery = useBuilder(() => {
return updateObservableQuery(new ObservableQuery(), query);
});
React.useEffect(
() => updateObservableQuery(observableQuery, query),
[observableQuery, query]
);
return observableQuery;
};
const updateObservableQuery = action((
observableQuery: ObservableQuery, tanstackQuery: TanstackQuery
) => {
observableQuery.data = tanstackQuery.data;
observableQuery.status = tanstackQuery.status;
});
The useObservableQuery
hook can simply be wrapped around the call to useQuery
,
for example: const getTodolists = useObservableQuery(useQuery(...))
.
In my actual code the useObservableQuery
hook also has a fetchAsLoad
flag that handles
the isFetching
state of the tanstack query. Since explaining this would take us to far into the details
of tanstack/query
I've omitted this flag.
We could use the same approach to wrap the result of useMutation
from @tanstack/query
in an ObservableMutation
object. However, since useMutation
doesn't provide any features
that I need, I prefer to replace it with my own useObservableMutation
hook (that uses a
similar API):
export class ObservableMutation {
@observable status = 'idle';
mutationFn: (args: any) => Promise<any> | void;
onMutate?: (args: any) => Promise<any> | void;
onSuccess?: (response: ObjT, args: any) => Promise<any> | void;
onError?: (args: any) => void;
@action setStatus = (status: MutationStatusT) => { this.status = status; };
mutateAsync = (args: any) => { /* Omitted for brevity */ };
constructor(args: ArgsT) {
this.mutationFn = args.mutationFn;
this.onSuccess = args.onSuccess;
this.onError = args.onError;
makeObservable(this);
}
}
export const isRunning = (observableMutation: ObservableMutation) => {
return observableMutation.status === 'loading';
};
export const useObservableMutation = (args: ArgsT) => {
return useBuilder(() => new ObservableMutation(args));
};
In the next chapter we'll look at how state-providers fetch data, add resource states and expose the resources to React components.