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.