RTK Query Caching

Compare a naive fetch-heavy implementation with an RTK Query version that dedupes network traffic and invalidates on mutation.

The demo instruments request counts, cache hits, and render counts so you can see the impact of the caching strategy in real time.

Here’s the code that powers the demo. Highlights show how the naïve implementation compares to the RTK Query version.

Naive list (hand-rolled fetch)

Each component runs its own network call inside useEffect. Every filter change refetches both lists and you manually juggle loading/error state.

export default function NaiveList({ filter, fetcher }: Props) {
  const [items, setItems] = useState<Item[] | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    async function run() {
      setLoading(true);
      setError(null);
      try {
        const res = await fetcher("/api/demos/items?filter=" + encodeURIComponent(filter) + "&delay=400&error=0");
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const data = (await res.json()) as { items: Item[] };
        if (!cancelled) setItems(data.items);
      } catch (e) {
        if (!cancelled) setError((e as Error).message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }
    run();
    return () => {
      cancelled = true;
    };
  }, [filter, fetcher]);

  return (
    <div>
      {loading ? "Fetching…" : `Loaded ${items?.length ?? 0} items`}
      {error && <div>{error}</div>}
      {/* render list */}
    </div>
  );
}

RTK Query API slice

The slice defines queries and mutations with tag-based invalidation. fetchBaseQuery is wrapped so the meter records network activity.

export const itemsApi = createApi({
  reducerPath: "itemsApi",
  baseQuery: fetchBaseQuery({ baseUrl: "/", fetchFn: trackedFetch }),
  tagTypes: ["Items"],
  endpoints: (build) => ({
    getItems: build.query<DemoItem[], QueryArgs>({
      query: ({ filter, delay = 200, error = 0 }) =>
        `api/demos/items?filter=${encodeURIComponent(filter)}&delay=${delay}&error=${error}`,
      transformResponse: (response: { items: DemoItem[] }) => response.items,
      keepUnusedDataFor: 60,
      providesTags: () => [{ type: "Items", id: "LIST" }],
    }),
    updateItem: build.mutation<DemoItem, UpdateArgs>({
      query: ({ id, delay = 200, ...body }) => ({
        url: `api/demos/items/${id}?delay=${delay}`,
        method: "PUT",
        body,
      }),
      invalidatesTags: [{ type: "Items", id: "LIST" }],
    }),
  }),
});

Store wiring

Each demo mounts its own RTK environment so we can compare metrics side-by-side. The tracked fetch is passed into the slice.

export function createRtkEnvironment(tracker: NetworkTracker) {
  const trackedFetch = createTrackedFetch(tracker, fetch);
  const api = createItemsApi(trackedFetch as typeof fetch);
  const store = configureStore({
    reducer: { [api.reducerPath]: api.reducer },
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware().concat(api.middleware),
    devTools: true,
  });

  return { store, api };
}

Want more? The full source lives in GitHub — links are available in the repository overview.

View the RTK Query demo on GitHub ↗