Skip to content

ReactQueryDevTools

Explanation

Let's build the ReactQueryDevTools for TanStack Query.

Requirements

  • Display the cached Query's status, staleTime, and gcTime information.
  • Refresh the list of cached Query objects whenever changes occur.

Solution

To detect changes in the cached Query objects inside the QueryCache, apply a subscription feature to QueryCache.

QueryCache

jsx
class QueryCache {
  listeners;

  constructor() {
    this.listeners = new Set();
  }

  subscribe = (listener) => {
    this.listeners.add(listener);

    const unsubscribe = () => {
      this.listeners.delete(listener);
    };

    return unsubscribe;
  };

  notify = () => {
    this.listeners.forEach((callback) => {
      callback();
    });
  };
}

Query

Query calls the notify method of QueryCache whenever the server state changes. This method publishes events to all subscribers registered to the QueryCache.

jsx
class Query {
  scheduleGcTimeout = () => {
    // ...
    this.gcTimeout = setTimeout(() => {
      this.cache.notify();
    }, gcTime);
  };

  setState() {
    // ...
    this.cache.notify();
  }
}

ReactQueryDevtools

ReactQueryDevtools accesses the list of cached Query objects through the QueryCache. It re-renders to update the Query list state whenever the server state changes.

jsx
const ReactQueryDevtools = () => {
  const queryClient = useQueryClient();

  const [, rerender] = useReducer((i) => i + 1, 0);

  useEffect(() => {
    return queryClient.cache.subscribe(rerender);
  }, [queryClient]);

  const queries = queryClient.getQueryCache().getAll();
  const sortedQueries = [...queries].sort((a, b) => (a.queryHash > b.queryHash ? 1 : -1));

  return (
    <div className="fixed bottom-0 w-full overflow-scroll text-white bg-black divide-y-2 divide-gray-800 divide-solid">
      {sortedQueries.map((query) => {
        const { queryKey, queryHash, state, observers, options } = query;
        const { isFetching, status } = state;

        const { staleTime, gcTime } = options;

        return (
          <div key={queryHash} className="p-2">
            {JSON.stringify(queryKey, null, 2)}, {JSON.stringify({ staleTime, gcTime }, null, 2)} -{" "}
            <span className="font-bold">
              {(() => {
                if (isFetching) {
                  return <span className="text-blue-500">fetching</span>;
                }

                if (!observers.length) {
                  return <span className="text-gray-500">inactive</span>;
                }

                if (status === "success") {
                  return <span className="text-green-500">success</span>;
                }

                if (status === "error") {
                  return <span className="text-red-500">error</span>;
                }

                return null;
              })()}
            </span>
          </div>
        );
      })}
    </div>
  );
};

Rendering ReactQueryDevtools in the top-level component allows you to verify that the DevTools work correctly.

jsx
const App = ({ children }) => {
  return (
    <QueryClientContext.Provider value={client}>
      <ReactQueryDevtools />
      {children}
    </QueryClientContext.Provider>
  );
};