ProvenPin Install

Redux Toolkit + RTK Query with tag invalidation and optimistic updates, tested with MSW: working versions (tested 2026-10-15)

An RTK Query API slice with auth headers from Redux state, tag-based cache invalidation and an optimistic update with rollback, tested offline in Vitest against MSW 3 request handlers without rendering React.

setup_failed0 passed · 1 failed2026-10-15 14:17 UTC
Status
setup_failed
Last passed
2026-10-15 14:17 UTC
Tests
0 / 1
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
@reduxjs/toolkit@2.13.0msw@3.0.3vitest@5.0.4@types/node@26.6.5
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38755782594

The code

Exactly the files that passed. Your agent can pull them with get_recipe("redux-toolkit-rtk-query-msw") (install), or see them on GitHub.

package.json
{
  "name": "redux-toolkit-rtk-query-msw",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
  },
  "dependencies": {
    "@reduxjs/toolkit": "2.13.0"
  },
  "devDependencies": {
    "msw": "3.0.3",
    "vitest": "5.0.4",
    "@types/node": "26.6.5"
  }
}
src/store.ts
import { configureStore, createSlice, type PayloadAction } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";

export type Todo = { id: number; title: string; done: boolean };

export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({
    baseUrl: "https://api.example.test/",
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token;
      if (token) headers.set("authorization", `Bearer ${token}`);
      return headers;
    },
  }),
  tagTypes: ["Todo"],
  endpoints: (b) => ({
    getTodos: b.query<Todo[], void>({
      query: () => "todos",
      providesTags: (r) => [...(r ?? []).map(({ id }) => ({ type: "Todo" as const, id })), { type: "Todo" as const, id: "LIST" }],
    }),
    addTodo: b.mutation<Todo, { title: string }>({
      query: (body) => ({ url: "todos", method: "POST", body }),
      invalidatesTags: [{ type: "Todo", id: "LIST" }], // refetch the list after a create
    }),
    toggleTodo: b.mutation<Todo, Todo>({
      query: (t) => ({ url: `todos/${t.id}`, method: "PATCH", body: { done: !t.done } }),
      // Optimistic update: flip the cached item now, undo if the server says no.
      async onQueryStarted(t, { dispatch, queryFulfilled }) {
        const patch = dispatch(api.util.updateQueryData("getTodos", undefined, (draft) => {
          const item = draft.find((d) => d.id === t.id);
          if (item) item.done = !t.done;
        }));
        try { await queryFulfilled; } catch { patch.undo(); }
      },
    }),
  }),
});

const auth = createSlice({
  name: "auth",
  initialState: { token: null as string | null },
  reducers: { signedIn: (s, a: PayloadAction<string>) => { s.token = a.payload; } },
});
export const { signedIn } = auth.actions;

export function makeStore() {
  return configureStore({
    reducer: { auth: auth.reducer, [api.reducerPath]: api.reducer },
    middleware: (gdm) => gdm().concat(api.middleware),
  });
}
export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
test/store.test.ts
import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
import { api, makeStore, signedIn, type Todo } from "../src/store.ts";

const seed = (): Todo[] => [{ id: 1, title: "seed", done: false }];
let todos: Todo[] = seed();
const seen: { method: string; auth: string | null }[] = [];

const server = setupServer(
  http.get("https://api.example.test/todos", ({ request }) => {
    seen.push({ method: "GET", auth: request.headers.get("authorization") });
    return HttpResponse.json(todos);
  }),
  http.post("https://api.example.test/todos", async ({ request }) => {
    const body = (await request.json()) as { title: string };
    const todo = { id: todos.length + 1, title: body.title, done: false };
    todos.push(todo);
    seen.push({ method: "POST", auth: request.headers.get("authorization") });
    return HttpResponse.json(todo, { status: 201 });
  }),
  http.patch("https://api.example.test/todos/:id", async ({ params, request }) => {
    const t = todos.find((x) => x.id === Number(params.id));
    if (!t) return HttpResponse.json({ message: "not found" }, { status: 404 });
    Object.assign(t, await request.json());
    return HttpResponse.json(t);
  }),
);

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => { server.resetHandlers(); todos = seed(); seen.length = 0; });
afterAll(() => server.close());

describe("RTK Query with MSW", () => {
  it("fetches into the cache and sends the auth header from state", async () => {
    const store = makeStore();
    store.dispatch(signedIn("t0k"));
    const r = await store.dispatch(api.endpoints.getTodos.initiate());
    expect(r.data).toEqual([{ id: 1, title: "seed", done: false }]);
    expect(seen).toEqual([{ method: "GET", auth: "Bearer t0k" }]);
    // A second subscriber is served from cache, no new request.
    await store.dispatch(api.endpoints.getTodos.initiate());
    expect(seen).toHaveLength(1);
  });

  it("a mutation invalidates the list tag and triggers a refetch", async () => {
    const store = makeStore();
    const sub = store.dispatch(api.endpoints.getTodos.initiate());
    await sub;
    await store.dispatch(api.endpoints.addTodo.initiate({ title: "write tests" }));
    await expect.poll(() => api.endpoints.getTodos.select()(store.getState()).data?.length).toBe(2);
    expect(seen.map((s) => s.method)).toEqual(["GET", "POST", "GET"]);
    sub.unsubscribe();
  });

  it("optimistic toggle shows immediately and rolls back on a server error", async () => {
    const store = makeStore();
    const sub = store.dispatch(api.endpoints.getTodos.initiate());
    await sub;
    server.use(http.patch("https://api.example.test/todos/:id", () => HttpResponse.json({ message: "nope" }, { status: 500 })));
    const pending = store.dispatch(api.endpoints.toggleTodo.initiate({ id: 1, title: "seed", done: false }));
    expect(api.endpoints.getTodos.select()(store.getState()).data?.[0].done).toBe(true); // optimistic
    const r = await pending;
    expect("error" in r && (r.error as { status: number }).status).toBe(500);
    expect(api.endpoints.getTodos.select()(store.getState()).data?.[0].done).toBe(false); // rolled back
    sub.unsubscribe();
  });

  it("surfaces HTTP errors as { status, data } on the query result", async () => {
    server.use(http.get("https://api.example.test/todos", () => HttpResponse.json({ message: "down" }, { status: 503 })));
    const r = await makeStore().dispatch(api.endpoints.getTodos.initiate());
    expect(r.error).toEqual({ status: 503, data: { message: "down" } });
  });
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.