Claude Code for SWR: React Hooks for Data Fetching — Claude Skills 360 Blog
Blog / Frontend / Claude Code for SWR: React Hooks for Data Fetching
Frontend

Claude Code for SWR: React Hooks for Data Fetching

Published: May 11, 2027
Read time: 6 min read
By: Claude Skills 360

SWR implements the stale-while-revalidate strategy — useSWR(key, fetcher) returns { data, error, isLoading }. The key is the cache key and URL. middleware objects wrap requests. SWRConfig sets global defaults with fetcher, onError, and revalidateOnFocus. mutate(key, data, { revalidate: false }) updates the cache optimistically. useSWRMutation(key, fetcher) returns { trigger, isMutating } for POST/PUT/DELETE. useSWRInfinite(getKey, fetcher) handles paginated lists with size and setSize. Conditional fetching uses null as key — useSWR(userId ? \/users/${userId}` : null, fetcher). swr/immutablefor data that never changes.preload(“/api/user”, fetcher)warms the cache before component mount.errorRetryCount: 3andonErrorRetrycustomize retry behavior. TypeScript:useSWR(“/api/user”, fetcher)`. Claude Code generates SWR hooks, optimistic mutation patterns, infinite scroll feeds, and global fetch configuration.

CLAUDE.md for SWR

## SWR Stack
- Version: swr >= 2.2
- Global: <SWRConfig value={{ fetcher: (url) => fetch(url).then(r => { if (!r.ok) throw r; return r.json() }), revalidateOnFocus: true }}>
- Query: const { data, error, isLoading } = useSWR<User>("/api/user")
- Mutate: const { mutate } = useSWR(...); mutate(optimisticData, { revalidate: false })
- Global mutate: mutate("/api/users") — refetch without hook ref
- Mutation: const { trigger, isMutating } = useSWRMutation("/api/users", async (url, { arg }) => poster(url, arg))
- Infinite: const { data, size, setSize } = useSWRInfinite((index) => `/api/posts?page=${index + 1}`)
- Conditional: useSWR(userId ? `/api/users/${userId}` : null, fetcher)

Global Configuration

// providers/SWRProvider.tsx — global SWR config
"use client"
import { SWRConfig, type SWRConfiguration } from "swr"
import { useCallback } from "react"

// Generic authenticated fetcher
async function apiFetcher<T>(url: string): Promise<T> {
  const res = await fetch(url, {
    credentials: "include",
    headers: { "Content-Type": "application/json" },
  })

  if (!res.ok) {
    const error = await res.json().catch(() => ({ message: "An error occurred" }))
    throw Object.assign(new Error(error.message ?? `HTTP ${res.status}`), {
      status: res.status,
      info: error,
    })
  }

  return res.json()
}

const swrConfig: SWRConfiguration = {
  fetcher: apiFetcher,
  revalidateOnFocus: true,
  revalidateOnReconnect: true,
  errorRetryCount: 3,
  errorRetryInterval: 1000,
  dedupingInterval: 2000,
  onError: (error) => {
    // Log errors but don't toast 401 (handled by redirect)
    if (error.status !== 401) {
      console.error("[SWR]", error.message)
    }
  },
}

export function SWRProvider({ children }: { children: React.ReactNode }) {
  return (
    <SWRConfig value={swrConfig}>
      {children}
    </SWRConfig>
  )
}

Typed Data Hooks

// hooks/useUser.ts — typed SWR hooks with mutations
import useSWR from "swr"
import useSWRMutation from "swr/mutation"

export type User = {
  id: string
  name: string
  email: string
  role: "user" | "admin"
  avatarUrl: string | null
  plan: "free" | "pro"
}

export type UpdateUserInput = Partial<Pick<User, "name" | "avatarUrl">>

// Read current user
export function useCurrentUser() {
  const { data, error, isLoading, mutate } = useSWR<User>("/api/auth/me")

  return {
    user: data,
    isLoading,
    isError: !!error,
    isUnauthorized: error?.status === 401,
    mutate,
  }
}

// Helper: POST/PUT/PATCH/DELETE
async function apiMutate<T>(url: string, { arg }: { arg: { method: string; body?: unknown } }): Promise<T> {
  const res = await fetch(url, {
    method: arg.method,
    credentials: "include",
    headers: { "Content-Type": "application/json" },
    body: arg.body ? JSON.stringify(arg.body) : undefined,
  })

  if (!res.ok) {
    const error = await res.json().catch(() => ({}))
    throw Object.assign(new Error(error.message ?? "Request failed"), { status: res.status })
  }

  return res.status === 204 ? null as T : res.json()
}

// Update user with optimistic update
export function useUpdateUser() {
  const { mutate: mutateUser } = useSWR<User>("/api/auth/me")

  const { trigger, isMutating } = useSWRMutation(
    "/api/auth/me",
    (url, { arg }: { arg: UpdateUserInput }) =>
      apiMutate<User>(url, { arg: { method: "PUT", body: arg } }),
    {
      onSuccess: (updated) => {
        // Update cache with server response
        mutateUser(updated, { revalidate: false })
      },
    },
  )

  const updateUser = async (input: UpdateUserInput) => {
    // Optimistic update
    await mutateUser(
      async (current) => {
        if (!current) return current
        return { ...current, ...input }
      },
      {
        optimisticData: (current) => current ? { ...current, ...input } : undefined,
        revalidate: false,
        rollbackOnError: true,
      },
    )
  }

  return { trigger, isMutating, updateUser }
}

Infinite Scroll Feed

// components/feed/InfiniteFeed.tsx — useSWRInfinite pagination
"use client"
import useSWRInfinite from "swr/infinite"
import { useIntersection } from "@/hooks/useIntersection"
import { useEffect, useRef } from "react"

type Post = {
  id: string
  title: string
  excerpt: string
  author: { name: string; avatarUrl: string }
  createdAt: string
}

type PostsPage = {
  items: Post[]
  nextCursor: string | null
}

const PAGE_SIZE = 20

export function InfiniteFeed() {
  const sentinelRef = useRef<HTMLDivElement>(null)
  const isVisible = useIntersection(sentinelRef)

  const { data, error, isLoading, size, setSize, isValidating } = useSWRInfinite<PostsPage>(
    (pageIndex, previousPageData) => {
      // Reached end
      if (previousPageData && !previousPageData.nextCursor) return null
      // First page
      if (pageIndex === 0) return `/api/posts?limit=${PAGE_SIZE}`
      // Subsequent pages — cursor-based
      return `/api/posts?limit=${PAGE_SIZE}&cursor=${previousPageData!.nextCursor}`
    },
    { revalidateFirstPage: false, parallel: false },
  )

  const posts = data?.flatMap(page => page.items) ?? []
  const hasMore = data?.[data.length - 1]?.nextCursor != null
  const isLoadingMore = size > 1 && data && typeof data[size - 1] === "undefined"

  // Auto-load on scroll
  useEffect(() => {
    if (isVisible && hasMore && !isValidating) {
      setSize(s => s + 1)
    }
  }, [isVisible, hasMore, isValidating, setSize])

  if (isLoading) {
    return (
      <div className="space-y-4">
        {Array.from({ length: 5 }).map((_, i) => (
          <div key={i} className="rounded-xl border p-4 space-y-3 animate-pulse">
            <div className="flex gap-3">
              <div className="size-10 rounded-full bg-muted" />
              <div className="space-y-2 flex-1">
                <div className="h-3 bg-muted rounded w-1/3" />
                <div className="h-3 bg-muted rounded w-1/4" />
              </div>
            </div>
            <div className="h-4 bg-muted rounded w-3/4" />
            <div className="h-4 bg-muted rounded w-1/2" />
          </div>
        ))}
      </div>
    )
  }

  if (error) return <p className="text-red-500 text-sm">Failed to load posts</p>

  return (
    <div className="space-y-4">
      {posts.map(post => (
        <article key={post.id} className="rounded-xl border p-4 space-y-3">
          <div className="flex items-center gap-3">
            <img src={post.author.avatarUrl} alt="" className="size-10 rounded-full object-cover" />
            <div>
              <p className="font-medium text-sm">{post.author.name}</p>
              <p className="text-xs text-muted-foreground">
                {new Date(post.createdAt).toLocaleDateString()}
              </p>
            </div>
          </div>
          <h2 className="font-semibold">{post.title}</h2>
          <p className="text-muted-foreground text-sm line-clamp-3">{post.excerpt}</p>
        </article>
      ))}

      {/* Intersection sentinel */}
      <div ref={sentinelRef} className="h-4" />

      {isLoadingMore && (
        <div className="flex justify-center py-4">
          <div className="size-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
        </div>
      )}

      {!hasMore && posts.length > 0 && (
        <p className="text-center text-sm text-muted-foreground py-4">You've reached the end</p>
      )}
    </div>
  )
}

Conditional and Dependent Queries

// hooks/useProductWithReviews.ts — dependent SWR queries
import useSWR from "swr"

type Product = { id: string; name: string; categoryId: string }
type Review = { id: string; rating: number; comment: string }
type Category = { id: string; name: string; slug: string }

export function useProductWithReviews(productId: string | undefined) {
  // Main resource
  const { data: product, isLoading: productLoading } = useSWR<Product>(
    productId ? `/api/products/${productId}` : null,
  )

  // Dependent — only fetch after product loads
  const { data: reviews, isLoading: reviewsLoading } = useSWR<Review[]>(
    product ? `/api/products/${product.id}/reviews` : null,
  )

  // Parallel dependent — fetch category alongside reviews
  const { data: category } = useSWR<Category>(
    product?.categoryId ? `/api/categories/${product.categoryId}` : null,
  )

  return {
    product,
    reviews,
    category,
    isLoading: productLoading || reviewsLoading,
  }
}

For the TanStack Query (React Query) alternative when more advanced cache control, background synchronization, mutation options, and QueryClient configuration are needed — TanStack Query has a richer API for mutation states, retry policies, and optimistic updates than SWR, but SWR is simpler and has a smaller bundle, see the TanStack Query guide. For the RTK Query alternative when you already use Redux and want server-state caching integrated in the same store with createApi endpoint definitions — RTK Query is better when your app uses Redux for client state and you want a single store for everything, see the RTK Query guide. The Claude Skills 360 bundle includes SWR skill sets covering hooks, mutations, and infinite pagination. Start with the free tier to try SWR 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