Claude Code for RTK Query: Data Fetching with Redux Toolkit — Claude Skills 360 Blog
Blog / Frontend / Claude Code for RTK Query: Data Fetching with Redux Toolkit
Frontend

Claude Code for RTK Query: Data Fetching with Redux Toolkit

Published: May 9, 2027
Read time: 7 min read
By: Claude Skills 360

RTK Query is Redux Toolkit’s built-in data fetching and caching solution — createApi({ baseQuery: fetchBaseQuery({ baseUrl }), endpoints: (builder) => ({ ... }) }) defines a service. builder.query<ReturnType, ArgType>({ query: (id) => \/items/${id}` })creates a GET endpoint.builder.mutation<ReturnType, ArgType>({ query: (body) => ({ url, method: “POST”, body }) })creates POST/PUT/DELETE. Generated hooks:useGetItemsQuery(), useGetItemQuery(id), useCreateItemMutation(). providesTags: [“Item”]marks what a query caches.invalidatesTags: [“Item”]clears matching caches after a mutation.transformResponse: (response) => response.datareshapes API responses.onQueryStartedimplements optimistic updates viaupdateQueryData. pollingInterval: 10000refetches every 10 seconds.api.util.prefetchpre-loads data.injectEndpoints` enables code-splitting. Claude Code generates RTK Query CRUD services, optimistic list updates, pagination, and mutation with rollback.

CLAUDE.md for RTK Query

## RTK Query Stack
- Version: @reduxjs/toolkit >= 2.3
- API: const api = createApi({ reducerPath: "api", baseQuery: fetchBaseQuery({ baseUrl: "/api/", prepareHeaders: (headers, { getState }) => { headers.set("authorization", `Bearer ${selectToken(getState())}`) } }), endpoints: ... })
- Query: builder.query<Product[], void>({ query: () => "products", providesTags: ["Product"] })
- Mutation: builder.mutation<Product, Partial<Product>>({ query: (body) => ({ url: "products", method: "POST", body }), invalidatesTags: ["Product"] })
- Hooks: const { data, isLoading, error } = useGetProductsQuery(); const [create, { isLoading: creating }] = useCreateProductMutation()
- Store: configureStore({ reducer: { [api.reducerPath]: api.reducer }, middleware: (gDM) => gDM().concat(api.middleware) })

API Service

// store/api/productsApi.ts — RTK Query product service
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
import type { RootState } from "@/store"

export type Product = {
  id: string
  name: string
  description: string
  price: number
  stock: number
  categoryId: string
  imageUrl: string
  isActive: boolean
  createdAt: string
}

export type CreateProductInput = Omit<Product, "id" | "createdAt">
export type UpdateProductInput = Partial<CreateProductInput> & { id: string }

type PaginatedProducts = {
  items: Product[]
  total: number
  page: number
  pageSize: number
}

type ProductFilters = {
  page?: number
  pageSize?: number
  categoryId?: string
  minPrice?: number
  maxPrice?: number
  search?: string
  isActive?: boolean
}

export const productsApi = createApi({
  reducerPath: "productsApi",
  baseQuery: fetchBaseQuery({
    baseUrl: "/api/",
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth?.token
      if (token) headers.set("authorization", `Bearer ${token}`)
      return headers
    },
  }),
  tagTypes: ["Product", "ProductDetail"],
  endpoints: (builder) => ({
    // GET paginated list
    getProducts: builder.query<PaginatedProducts, ProductFilters>({
      query: (filters = {}) => ({
        url: "products",
        params: filters,
      }),
      providesTags: (result) =>
        result
          ? [
              ...result.items.map(({ id }) => ({ type: "Product" as const, id })),
              { type: "Product", id: "LIST" },
            ]
          : [{ type: "Product", id: "LIST" }],
    }),

    // GET single product
    getProduct: builder.query<Product, string>({
      query: (id) => `products/${id}`,
      providesTags: (_, __, id) => [{ type: "ProductDetail", id }],
    }),

    // POST create
    createProduct: builder.mutation<Product, CreateProductInput>({
      query: (body) => ({
        url: "products",
        method: "POST",
        body,
      }),
      invalidatesTags: [{ type: "Product", id: "LIST" }],
    }),

    // PUT update with optimistic update
    updateProduct: builder.mutation<Product, UpdateProductInput>({
      query: ({ id, ...body }) => ({
        url: `products/${id}`,
        method: "PUT",
        body,
      }),
      // Optimistic update
      async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
        const patchResult = dispatch(
          productsApi.util.updateQueryData("getProduct", id, (draft) => {
            Object.assign(draft, patch)
          }),
        )
        try {
          await queryFulfilled
        } catch {
          patchResult.undo()
        }
      },
      invalidatesTags: (_, __, { id }) => [
        { type: "Product", id },
        { type: "ProductDetail", id },
      ],
    }),

    // DELETE
    deleteProduct: builder.mutation<void, string>({
      query: (id) => ({
        url: `products/${id}`,
        method: "DELETE",
      }),
      invalidatesTags: (_, __, id) => [
        { type: "Product", id },
        { type: "Product", id: "LIST" },
      ],
    }),

    // PATCH toggle active — updates list optimistically
    toggleProductActive: builder.mutation<Product, { id: string; isActive: boolean }>({
      query: ({ id, isActive }) => ({
        url: `products/${id}/active`,
        method: "PATCH",
        body: { isActive },
      }),
      async onQueryStarted({ id, isActive }, { dispatch, queryFulfilled, getState }) {
        // Update all cached list queries that contain this product
        const patches: Array<ReturnType<typeof productsApi.util.updateQueryData>> = []

        for (const { endpointName, originalArgs } of productsApi.util.selectInvalidatedBy(
          getState(),
          [{ type: "Product", id }],
        )) {
          if (endpointName === "getProducts") {
            patches.push(
              dispatch(
                productsApi.util.updateQueryData("getProducts", originalArgs, (draft) => {
                  const item = draft.items.find(p => p.id === id)
                  if (item) item.isActive = isActive
                }),
              ),
            )
          }
        }

        try {
          await queryFulfilled
        } catch {
          patches.forEach(p => p.undo())
        }
      },
      invalidatesTags: (_, __, { id }) => [{ type: "Product", id }],
    }),
  }),
})

export const {
  useGetProductsQuery,
  useGetProductQuery,
  useCreateProductMutation,
  useUpdateProductMutation,
  useDeleteProductMutation,
  useToggleProductActiveMutation,
} = productsApi

Store Setup

// store/index.ts — Redux store with RTK Query
import { configureStore } from "@reduxjs/toolkit"
import { productsApi } from "./api/productsApi"
import authReducer from "./slices/authSlice"

export const store = configureStore({
  reducer: {
    auth: authReducer,
    [productsApi.reducerPath]: productsApi.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(productsApi.middleware),
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

React Components

// components/products/ProductsPage.tsx — full CRUD UI
"use client"
import { useState } from "react"
import {
  useGetProductsQuery,
  useDeleteProductMutation,
  useToggleProductActiveMutation,
} from "@/store/api/productsApi"

export function ProductsPage() {
  const [page, setPage] = useState(1)
  const [search, setSearch] = useState("")

  const { data, isLoading, isFetching } = useGetProductsQuery({
    page,
    pageSize: 20,
    search: search || undefined,
  })

  const [deleteProduct] = useDeleteProductMutation()
  const [toggleActive] = useToggleProductActiveMutation()

  if (isLoading) {
    return (
      <div className="space-y-3">
        {Array.from({ length: 5 }).map((_, i) => (
          <div key={i} className="h-16 rounded-xl bg-muted animate-pulse" />
        ))}
      </div>
    )
  }

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-3">
        <input
          type="search"
          placeholder="Search products..."
          value={search}
          onChange={e => { setSearch(e.target.value); setPage(1) }}
          className="input flex-1"
        />
        {isFetching && (
          <div className="size-5 border-2 border-primary border-t-transparent rounded-full animate-spin" />
        )}
      </div>

      <div className="space-y-2">
        {data?.items.map(product => (
          <div key={product.id} className="flex items-center gap-4 p-4 rounded-xl border">
            <img src={product.imageUrl} alt="" className="size-12 rounded-lg object-cover" />
            <div className="flex-1 min-w-0">
              <p className="font-medium truncate">{product.name}</p>
              <p className="text-sm text-muted-foreground">${(product.price / 100).toFixed(2)}</p>
            </div>
            <div className="flex items-center gap-2">
              <button
                onClick={() => toggleActive({ id: product.id, isActive: !product.isActive })}
                className={`text-xs px-2 py-1 rounded-full ${product.isActive ? "bg-green-100 text-green-700" : "bg-muted text-muted-foreground"}`}
              >
                {product.isActive ? "Active" : "Inactive"}
              </button>
              <button
                onClick={() => deleteProduct(product.id)}
                className="text-sm text-destructive hover:opacity-70 px-2 py-1"
              >
                Delete
              </button>
            </div>
          </div>
        ))}
      </div>

      {/* Pagination */}
      {data && data.total > 20 && (
        <div className="flex items-center justify-between">
          <p className="text-sm text-muted-foreground">
            {data.total} products
          </p>
          <div className="flex gap-2">
            <button onClick={() => setPage(p => p - 1)} disabled={page === 1} className="btn-ghost text-sm px-3 py-1.5 disabled:opacity-40">← Prev</button>
            <span className="text-sm px-3 py-1.5">Page {page}</span>
            <button onClick={() => setPage(p => p + 1)} disabled={page * 20 >= data.total} className="btn-ghost text-sm px-3 py-1.5 disabled:opacity-40">Next →</button>
          </div>
        </div>
      )}
    </div>
  )
}

For the TanStack Query (React Query) alternative when you don’t use Redux and want a standalone server-state library without the Redux boilerplate, store setup, and provider nesting — TanStack Query has a simpler API and is a better fit when you only need server-state caching without Redux’s client state management, see the TanStack Query guide. For the SWR alternative when a minimal, opinionated data-fetching library with automatic revalidation, focus-revalidation, and a tiny bundle footprint is preferred over RTK Query’s Redux integration — SWR requires no store setup and suits simple fetching without CRUD mutation patterns, see the SWR guide. The Claude Skills 360 bundle includes RTK Query skill sets covering endpoints, optimistic updates, and cache invalidation. Start with the free tier to try Redux data fetching generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free