Separating state from presentation using state providers
August 2023, by Maarten Nieber
Observing queries and mutations

ObservableQuery and the useQuery function

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:

ObservableQuery.ts
hooks/useObservableQuery.ts
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;
};

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.

The useMutation function

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):

ObservableMutation.ts
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));
};

What's next

In the next chapter we'll look at how state-providers fetch data, add resource states and expose the resources to React components.