From 556da03e5c72bc0d6d46600ec2d4c4b46850a15f Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Fri, 13 Feb 2026 11:29:53 -0800 Subject: [PATCH] Draft skill --- skills/swr/SKILL.md | 82 ++++ skills/swr/references/api.md | 250 +++++++++++ skills/swr/references/patterns.md | 507 +++++++++++++++++++++++ skills/swr/references/troubleshooting.md | 248 +++++++++++ 4 files changed, 1087 insertions(+) create mode 100644 skills/swr/SKILL.md create mode 100644 skills/swr/references/api.md create mode 100644 skills/swr/references/patterns.md create mode 100644 skills/swr/references/troubleshooting.md diff --git a/skills/swr/SKILL.md b/skills/swr/SKILL.md new file mode 100644 index 000000000..b4ff01456 --- /dev/null +++ b/skills/swr/SKILL.md @@ -0,0 +1,82 @@ +--- +name: swr +description: > + SWR (stale-while-revalidate) data fetching for React. Use when working with + SWR hooks (useSWR, useSWRMutation, useSWRInfinite, useSWRSubscription, + useSWRImmutable), configuring SWR providers, implementing data fetching + patterns, mutations, optimistic UI, infinite loading, pagination, prefetching, + polling, SSR/Next.js integration, caching, middleware, error handling, or + debugging SWR behavior (stale data, unexpected revalidation, hydration issues). + Also use for migrating between SWR versions. +--- + +# SWR + +React Hooks for data fetching with built-in caching, revalidation, focus tracking, and request deduplication. + +## Quick Reference + +```tsx +import useSWR from 'swr' // Core hook +import useSWRMutation from 'swr/mutation' // Remote mutations +import useSWRInfinite from 'swr/infinite' // Pagination / infinite loading +import useSWRImmutable from 'swr/immutable' // Never-revalidate data +import useSWRSubscription from 'swr/subscription' // Realtime subscriptions +import { SWRConfig, useSWRConfig, mutate, preload, unstable_serialize } from 'swr' +``` + +## Core Usage + +```tsx +const fetcher = (url: string) => fetch(url).then(r => r.json()) + +// Basic +const { data, error, isLoading, isValidating, mutate } = useSWR('/api/user', fetcher) + +// Conditional (pass null to skip) +const { data } = useSWR(userId ? `/api/users/${userId}` : null, fetcher) + +// Mutation +const { trigger, isMutating } = useSWRMutation('/api/todos', (url, { arg }: { arg: Todo }) => + fetch(url, { method: 'POST', body: JSON.stringify(arg) }).then(r => r.json()) +) + +// Optimistic update +await mutate( + fetch('/api/todos', { method: 'POST', body: JSON.stringify(todo) }), + { optimisticData: [...data, todo], rollbackOnError: true, revalidate: false } +) + +// Infinite +const { data, size, setSize } = useSWRInfinite( + (i, prev) => prev?.length === 0 ? null : `/api/posts?page=${i}`, + fetcher +) +``` + +## Key Concepts + +- **Key**: unique string, array, or object identifying data. Pass `null`/`undefined`/`false` to disable fetching +- **Deduplication**: identical keys within 2s share one request +- **Revalidation**: automatic on focus, reconnect, mount, and interval +- **Cache**: shared across components using same key. Customize via `SWRConfig` provider +- **isLoading vs isValidating**: `isLoading` = first load (no data), `isValidating` = any in-flight request + +## SWRConfig Provider + +```tsx + + + +``` + +## Detailed References + +- **Full API (hooks, config, types, cache)**: See [references/api.md](references/api.md) +- **Common patterns (SSR, infinite scroll, optimistic UI, middleware, custom hooks)**: See [references/patterns.md](references/patterns.md) +- **Troubleshooting & migration**: See [references/troubleshooting.md](references/troubleshooting.md) diff --git a/skills/swr/references/api.md b/skills/swr/references/api.md new file mode 100644 index 000000000..5d38e802f --- /dev/null +++ b/skills/swr/references/api.md @@ -0,0 +1,250 @@ +# SWR API Reference + +## Table of Contents + +- [Hooks](#hooks) +- [Configuration Options](#configuration-options) +- [Mutation Options](#mutation-options) +- [Infinite Configuration](#infinite-configuration) +- [Key Types](#key-types) +- [Response Types](#response-types) +- [Cache API](#cache-api) +- [Utility Functions](#utility-functions) + +## Hooks + +### useSWR + +```tsx +import useSWR from 'swr' +const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher?, config?) +``` + +**Overloads:** key only, key + fetcher, key + config, key + fetcher + config. + +### useSWRMutation + +```tsx +import useSWRMutation from 'swr/mutation' +const { trigger, data, error, isMutating, reset } = useSWRMutation(key, fetcher, options?) +``` + +`trigger(arg?, options?)` — invoke the mutation. `arg` is passed as `{ arg }` to the fetcher's second parameter. + +Fetcher signature: `(key, { arg }) => Promise` + +### useSWRInfinite + +```tsx +import useSWRInfinite from 'swr/infinite' +const { data, size, setSize, mutate, isLoading, isValidating } = useSWRInfinite(getKey, fetcher?, config?) +``` + +`getKey(pageIndex, previousPageData)` — return null to stop fetching. `data` is an array of page results. `size` / `setSize` control page count. + +### useSWRSubscription + +```tsx +import useSWRSubscription from 'swr/subscription' +const { data, error } = useSWRSubscription(key, (key, { next }) => { + // subscribe and call next(err, data) on updates + return () => { /* cleanup */ } +}, config?) +``` + +### useSWRImmutable + +```tsx +import useSWRImmutable from 'swr/immutable' +const { data } = useSWRImmutable(key, fetcher?) +``` + +Shorthand for `useSWR` with `revalidateIfStale: false`, `revalidateOnFocus: false`, `revalidateOnReconnect: false`. + +### useSWRConfig + +```tsx +import { useSWRConfig } from 'swr' +const { mutate, cache, ...config } = useSWRConfig() +``` + +Access the global SWR configuration, cache, and scoped mutate function. + +## Configuration Options + +### SWRConfiguration + +| Option | Type | Default | Description | +|---|---|---|---| +| `fetcher` | `BareFetcher` | — | Default fetcher function | +| `revalidateOnFocus` | `boolean` | `true` | Revalidate when window gains focus | +| `revalidateOnReconnect` | `boolean` | `true` | Revalidate on network recovery | +| `revalidateOnMount` | `boolean` | `undefined` | Revalidate when component mounts | +| `revalidateIfStale` | `boolean` | `true` | Revalidate if data is stale | +| `refreshInterval` | `number \| ((data) => number)` | `0` | Polling interval in ms (0 = disabled) | +| `refreshWhenHidden` | `boolean` | `false` | Poll when page is hidden | +| `refreshWhenOffline` | `boolean` | `false` | Poll when offline | +| `shouldRetryOnError` | `boolean \| ((err) => boolean)` | `true` | Retry on error | +| `errorRetryCount` | `number` | `undefined` | Max retry count | +| `errorRetryInterval` | `number` | `5000` | Retry interval in ms | +| `dedupingInterval` | `number` | `2000` | Dedup requests within this window | +| `focusThrottleInterval` | `number` | `5000` | Throttle focus revalidation | +| `loadingTimeout` | `number` | `3000` | Timeout before `onLoadingSlow` fires | +| `suspense` | `boolean` | `false` | Enable React Suspense mode | +| `fallbackData` | `Data` | — | Initial data (per-hook) | +| `fallback` | `Record` | `{}` | Fallback data map (provider-level) | +| `keepPreviousData` | `boolean` | `false` | Keep previous data when key changes | +| `isPaused` | `() => boolean` | `() => false` | Pause all revalidation | +| `compare` | `(a, b) => boolean` | `dequal` | Custom comparison function | +| `use` | `Middleware[]` | — | Middleware array | +| `provider` | `(cache) => Cache` | — | Custom cache provider | +| `onSuccess` | `(data, key, config) => void` | — | Success callback | +| `onError` | `(err, key, config) => void` | — | Error callback | +| `onErrorRetry` | `(err, key, config, revalidate, opts) => void` | — | Custom retry logic | +| `onLoadingSlow` | `(key, config) => void` | — | Slow loading callback | +| `onDiscarded` | `(key) => void` | — | Request discarded callback | + +## Mutation Options + +### MutatorOptions (bound mutate) + +| Option | Type | Default | Description | +|---|---|---|---| +| `revalidate` | `boolean \| ((data, key) => boolean)` | `true` | Revalidate after mutation | +| `populateCache` | `boolean \| ((result, current) => Data)` | `true` | Update cache with result | +| `optimisticData` | `Data \| ((current, displayed) => Data)` | — | Optimistic UI data | +| `rollbackOnError` | `boolean \| ((err) => boolean)` | `true` | Rollback on error | +| `throwOnError` | `boolean` | `false` | Throw instead of returning error | + +### SWRMutationConfiguration (useSWRMutation) + +Same as MutatorOptions plus `onSuccess` and `onError` callbacks. + +## Infinite Configuration + +Extends SWRConfiguration with: + +| Option | Type | Default | Description | +|---|---|---|---| +| `initialSize` | `number` | `1` | Initial number of pages | +| `revalidateAll` | `boolean` | `false` | Revalidate all pages | +| `persistSize` | `boolean` | `false` | Keep size on key change | +| `revalidateFirstPage` | `boolean` | `true` | Revalidate first page on mutation | +| `parallel` | `boolean` | `false` | Fetch pages in parallel | + +## Key Types + +```tsx +// Key can be string, array, object, or null/undefined/false to disable +type Key = Arguments | (() => Arguments) +type Arguments = string | readonly [any, ...unknown[]] | Record | null | undefined | false +``` + +**Conditional fetching:** pass `null`, `undefined`, or `false` to skip the request. + +**Function keys:** `() => condition ? '/api/data' : null` — re-evaluated on render. + +**Array keys:** `['/api/user', id]` — all elements are passed to the fetcher. + +## Response Types + +### SWRResponse + +```tsx +{ + data: Data | undefined // Fetched data + error: Error | undefined // Error object + isLoading: boolean // First load, no data yet + isValidating: boolean // Any request in flight + mutate: KeyedMutator // Bound mutate function +} +``` + +`isLoading` is true only on the initial load (no cached data). `isValidating` is true whenever a request is in flight (including revalidation). + +### SWRMutationResponse + +```tsx +{ + data: Data | undefined + error: Error | undefined + isMutating: boolean + trigger: (arg?, opts?) => Promise + reset: () => void +} +``` + +### SWRInfiniteResponse + +```tsx +extends SWRResponse { + size: number + setSize: (size: number | ((size: number) => number)) => Promise +} +``` + +## Cache API + +```tsx +interface Cache { + keys(): IterableIterator + get(key: string): State | undefined + set(key: string, value: State): void + delete(key: string): void +} +``` + +Use `provider` option in `SWRConfig` to supply a custom cache: + +```tsx + new Map() }}> + {children} + +``` + +## Utility Functions + +### preload + +```tsx +import { preload } from 'swr' +await preload(key, fetcher) +``` + +Preload data before components mount. Returns the fetcher promise. + +### unstable_serialize + +```tsx +import { unstable_serialize } from 'swr' // for useSWR keys +import { unstable_serialize } from 'swr/infinite' // for useSWRInfinite keys +``` + +Serialize a key for use in `fallback` maps. + +### SWRConfig (Provider) + +```tsx +import { SWRConfig } from 'swr' + + + {children} + + +// Functional config (access parent) + ({ ...parent, fetcher })}> + {children} + +``` + +### Global mutate + +```tsx +import { mutate } from 'swr' + +// Mutate specific key +mutate('/api/user', newData, opts?) + +// Mutate by filter +mutate(key => key.startsWith('/api/'), newData, opts?) +``` diff --git a/skills/swr/references/patterns.md b/skills/swr/references/patterns.md new file mode 100644 index 000000000..85ed133fd --- /dev/null +++ b/skills/swr/references/patterns.md @@ -0,0 +1,507 @@ +# SWR Common Patterns + +## Table of Contents + +- [Data Fetching](#data-fetching) +- [Mutations & Optimistic UI](#mutations--optimistic-ui) +- [Infinite Loading & Pagination](#infinite-loading--pagination) +- [Prefetching & Preloading](#prefetching--preloading) +- [SSR & Next.js Integration](#ssr--nextjs-integration) +- [Subscriptions & Realtime](#subscriptions--realtime) +- [Custom Hooks](#custom-hooks) +- [Middleware](#middleware) +- [Error Handling](#error-handling) +- [Performance Patterns](#performance-patterns) + +## Data Fetching + +### Basic fetch + +```tsx +import useSWR from 'swr' + +const fetcher = (url: string) => fetch(url).then(r => r.json()) + +function Profile() { + const { data, error, isLoading } = useSWR('/api/user', fetcher) + + if (error) return
Failed to load
+ if (isLoading) return
Loading...
+ return
{data.name}
+} +``` + +### Conditional fetching + +```tsx +// Fetch only when user exists +const { data } = useSWR(user ? `/api/user/${user.id}` : null, fetcher) + +// Function key for derived conditions +const { data } = useSWR(() => `/api/user/${user.id}/posts`, fetcher) +// Throws if user is undefined, SWR catches and pauses +``` + +### Multiple arguments + +```tsx +const { data } = useSWR( + ['/api/user', id, token], + ([url, userId, authToken]) => + fetch(`${url}/${userId}`, { headers: { Authorization: authToken } }).then(r => r.json()) +) +``` + +### Global fetcher + +```tsx + fetch(url).then(r => r.json()) }}> + + + +// Now omit fetcher from individual hooks +const { data } = useSWR('/api/user') +``` + +### Axios + +```tsx +import axios from 'axios' + +const fetcher = (url: string) => axios.get(url).then(res => res.data) +const { data } = useSWR('/api/user', fetcher) +``` + +### Polling + +```tsx +const { data } = useSWR('/api/realtime', fetcher, { + refreshInterval: 1000, // Poll every second +}) + +// Dynamic interval based on data +const { data } = useSWR('/api/data', fetcher, { + refreshInterval: (data) => (data?.isActive ? 1000 : 5000), +}) +``` + +## Mutations & Optimistic UI + +### Bound mutate (revalidation) + +```tsx +const { data, mutate } = useSWR('/api/todos', fetcher) + +async function addTodo(text: string) { + await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }) }) + mutate() // Revalidate +} +``` + +### Optimistic update + +```tsx +const { data, mutate } = useSWR('/api/todos', fetcher) + +async function addTodo(newTodo: Todo) { + await mutate( + fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) }).then(r => r.json()), + { + optimisticData: [...(data ?? []), newTodo], + rollbackOnError: true, + populateCache: (result, current) => [...(current ?? []), result], + revalidate: false, + } + ) +} +``` + +### useSWRMutation for remote mutations + +```tsx +import useSWRMutation from 'swr/mutation' + +async function createTodo(url: string, { arg }: { arg: { text: string } }) { + return fetch(url, { method: 'POST', body: JSON.stringify(arg) }).then(r => r.json()) +} + +function TodoForm() { + const { trigger, isMutating } = useSWRMutation('/api/todos', createTodo) + + return ( + + ) +} +``` + +### Optimistic mutation with useSWRMutation + +```tsx +const { trigger } = useSWRMutation('/api/todos', createTodo, { + optimisticData: (current) => [...(current ?? []), optimisticTodo], + rollbackOnError: true, +}) +``` + +### Global mutate + +```tsx +import { mutate } from 'swr' + +// Revalidate all keys matching a pattern +mutate(key => typeof key === 'string' && key.startsWith('/api/todos')) + +// Update specific key from anywhere +mutate('/api/user', newUserData) +``` + +## Infinite Loading & Pagination + +### Basic infinite list + +```tsx +import useSWRInfinite from 'swr/infinite' + +const PAGE_SIZE = 10 + +function Posts() { + const getKey = (pageIndex: number, previousPageData: Post[] | null) => { + if (previousPageData && previousPageData.length === 0) return null // End + return `/api/posts?page=${pageIndex}&limit=${PAGE_SIZE}` + } + + const { data, size, setSize, isLoading, isValidating } = useSWRInfinite(getKey, fetcher) + + const posts = data ? data.flat() : [] + const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined') + const isEmpty = data?.[0]?.length === 0 + const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < PAGE_SIZE) + + return ( + <> + {posts.map(post => )} + + + ) +} +``` + +### Infinite scroll with IntersectionObserver + +```tsx +const ref = useRef(null) + +useEffect(() => { + if (!ref.current) return + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting && !isReachingEnd && !isValidating) { + setSize(s => s + 1) + } + }) + observer.observe(ref.current) + return () => observer.disconnect() +}, [isReachingEnd, isValidating, setSize]) + +return ( + <> + {posts.map(post => )} +
+ +) +``` + +### Parallel fetching + +```tsx +const { data } = useSWRInfinite(getKey, fetcher, { + parallel: true, // Fetch all pages simultaneously on revalidation +}) +``` + +## Prefetching & Preloading + +### preload API + +```tsx +import { preload } from 'swr' + +// Preload before render +preload('/api/user', fetcher) + +function App() { + const { data } = useSWR('/api/user', fetcher) // Uses preloaded data +} +``` + +### Prefetch on hover + +```tsx +function Link({ href, children }: { href: string; children: React.ReactNode }) { + return ( + preload(href, fetcher)} + > + {children} + + ) +} +``` + +### Prefetch related data + +```tsx +const { data: user } = useSWR('/api/user', fetcher) + +useEffect(() => { + if (user) preload(`/api/user/${user.id}/posts`, fetcher) +}, [user]) +``` + +## SSR & Next.js Integration + +### App Router (Server Components) + +```tsx +// app/page.tsx +import { SWRConfig, unstable_serialize } from 'swr' + +export default async function Page() { + const data = await fetch('https://api.example.com/user').then(r => r.json()) + + return ( + + + + ) +} +``` + +### Pages Router (getServerSideProps) + +```tsx +export async function getServerSideProps() { + const data = await fetchUser() + return { props: { fallback: { '/api/user': data } } } +} + +export default function Page({ fallback }: { fallback: Record }) { + return ( + + + + ) +} +``` + +### Per-hook fallback + +```tsx +const { data } = useSWR('/api/user', fetcher, { + fallbackData: serverData, +}) +``` + +## Subscriptions & Realtime + +### WebSocket + +```tsx +import useSWRSubscription from 'swr/subscription' + +function LivePrice({ symbol }: { symbol: string }) { + const { data } = useSWRSubscription(`price-${symbol}`, (key, { next }) => { + const ws = new WebSocket(`wss://stream.example.com/${symbol}`) + ws.onmessage = (e) => next(null, JSON.parse(e.data)) + ws.onerror = (e) => next(e as Error) + return () => ws.close() + }) + + return {data?.price} +} +``` + +### EventSource (SSE) + +```tsx +const { data } = useSWRSubscription('/api/events', (key, { next }) => { + const es = new EventSource(key) + es.onmessage = (e) => next(null, JSON.parse(e.data)) + es.onerror = (e) => next(e as Error) + return () => es.close() +}) +``` + +## Custom Hooks + +Wrap useSWR in domain-specific hooks: + +```tsx +function useUser(id: string) { + return useSWR(`/api/users/${id}`, fetcher) +} + +function usePosts(userId: string) { + return useSWR(userId ? `/api/users/${userId}/posts` : null, fetcher) +} + +function useInfinitePosts(userId: string) { + return useSWRInfinite( + (index, prev) => { + if (prev && prev.length === 0) return null + return `/api/users/${userId}/posts?page=${index}` + }, + fetcher + ) +} +``` + +## Middleware + +### Structure + +```tsx +import type { Middleware } from 'swr' + +const myMiddleware: Middleware = (useSWRNext) => (key, fetcher, config) => { + // Before hook logic + const swr = useSWRNext(key, fetcher, config) + // After hook logic + return swr +} + +// Usage +const { data } = useSWR(key, fetcher, { use: [myMiddleware] }) +``` + +### Logger middleware + +```tsx +const logger: Middleware = (useSWRNext) => (key, fetcher, config) => { + const swr = useSWRNext(key, fetcher, config) + useEffect(() => { + console.log('SWR:', key, swr.data) + }, [key, swr.data]) + return swr +} +``` + +### Serialize keys middleware + +```tsx +const serialize: Middleware = (useSWRNext) => (key, fetcher, config) => { + const serializedKey = Array.isArray(key) ? JSON.stringify(key) : key + return useSWRNext(serializedKey, fetcher, config) +} +``` + +## Error Handling + +### Basic error handling + +```tsx +const { data, error } = useSWR(key, fetcher) +if (error) return +``` + +### Custom retry + +```tsx +const { data } = useSWR(key, fetcher, { + onErrorRetry: (error, key, config, revalidate, { retryCount }) => { + if (error.status === 404) return // Don't retry 404s + if (retryCount >= 3) return // Max 3 retries + setTimeout(() => revalidate({ retryCount }), 5000) + }, +}) +``` + +### Global error handler + +```tsx + { + if (error.status !== 403 && error.status !== 404) { + reportError(error, key) + } + } +}}> + + +``` + +## Performance Patterns + +### Deduplication + +SWR deduplicates requests with the same key within `dedupingInterval` (default 2s). Multiple components using the same key share one request. + +### Keep previous data + +```tsx +const { data } = useSWR(searchKey, fetcher, { + keepPreviousData: true, // Show old data while fetching new key +}) +``` + +### Immutable data + +```tsx +import useSWRImmutable from 'swr/immutable' + +// Never revalidates — use for static data +const { data } = useSWRImmutable('/api/config', fetcher) +``` + +### Suspense + +```tsx +import { Suspense } from 'react' + +function Profile() { + const { data } = useSWR('/api/user', fetcher, { suspense: true }) + return
{data.name}
// data is guaranteed +} + +function App() { + return ( + }> + + + ) +} +``` + +### Custom cache provider + +```tsx + { + const map = new Map(JSON.parse(localStorage.getItem('swr-cache') || '[]')) + window.addEventListener('beforeunload', () => { + localStorage.setItem('swr-cache', JSON.stringify([...map.entries()])) + }) + return map + } +}}> + + +``` + +### Pausing revalidation + +```tsx +const { data } = useSWR(key, fetcher, { + isPaused: () => !isOnline, // Pause when offline +}) +``` diff --git a/skills/swr/references/troubleshooting.md b/skills/swr/references/troubleshooting.md new file mode 100644 index 000000000..708e68c05 --- /dev/null +++ b/skills/swr/references/troubleshooting.md @@ -0,0 +1,248 @@ +# SWR Troubleshooting + +## Table of Contents + +- [Unexpected Revalidation](#unexpected-revalidation) +- [Stale or Missing Data](#stale-or-missing-data) +- [Infinite Loops](#infinite-loops) +- [SSR Issues](#ssr-issues) +- [TypeScript Issues](#typescript-issues) +- [Performance Issues](#performance-issues) +- [Migration from v1 to v2](#migration-from-v1-to-v2) + +## Unexpected Revalidation + +### Data refetches when switching tabs + +SWR revalidates on focus by default. Disable with: + +```tsx +useSWR(key, fetcher, { revalidateOnFocus: false }) +// Or globally + +``` + +### Data refetches on component mount + +SWR revalidates when a component using a cached key mounts. Control with: + +```tsx +useSWR(key, fetcher, { revalidateOnMount: false }) // Skip mount revalidation +useSWR(key, fetcher, { revalidateIfStale: false }) // Skip if data exists +``` + +Or use `useSWRImmutable` for data that never needs revalidation. + +### Multiple identical requests + +SWR deduplicates within `dedupingInterval` (2s default). If you see duplicates: + +- Check that keys are stable (no new object/array references each render) +- Increase `dedupingInterval` if needed + +### Revalidation after mutation + +`mutate()` revalidates by default. Pass `{ revalidate: false }` to skip: + +```tsx +mutate(key, newData, { revalidate: false }) +``` + +## Stale or Missing Data + +### Data is undefined after mutation + +Ensure `populateCache` is configured correctly: + +```tsx +mutate(key, newData, { + populateCache: true, // Boolean: replace cache with mutation result + revalidate: false, // Skip revalidation if cache is already correct +}) +``` + +### Cache not shared between components + +Components must use the same serialized key. Object/array keys are compared by value via `stableHash`, but function keys must return the same value: + +```tsx +// These share cache (same serialized key): +useSWR('/api/user', fetcher) +useSWR('/api/user', fetcher) + +// These also share cache: +useSWR(['/api', 'user'], fetcher) +useSWR(['/api', 'user'], fetcher) +``` + +### keepPreviousData shows wrong data + +`keepPreviousData` keeps the last resolved data while a new key is loading. Clear it by setting the key to `null` first if needed. + +## Infinite Loops + +### Hook called in a loop + +Never construct new object/array keys inline without memoization: + +```tsx +// BAD: New array reference every render → infinite revalidation +useSWR(['/api', { id }], fetcher) + +// GOOD: Stable key +useSWR(`/api?id=${id}`, fetcher) + +// GOOD: Array with primitives only +useSWR(['/api', id], fetcher) +``` + +### Mutation triggers rerender loop + +Avoid calling `mutate()` during render. Use it in event handlers or effects: + +```tsx +// BAD +function Component() { + const { data, mutate } = useSWR(key, fetcher) + if (data?.needsUpdate) mutate() // Loop! + + // GOOD + useEffect(() => { + if (data?.needsUpdate) mutate() + }, [data?.needsUpdate]) +} +``` + +## SSR Issues + +### Hydration mismatch + +Use `fallbackData` or `SWRConfig.fallback` to provide server data: + +```tsx +// Per-hook +useSWR(key, fetcher, { fallbackData: serverData }) + +// Provider-level + +``` + +### Data fetched twice (server + client) + +This is expected — SWR revalidates on mount by default to ensure freshness. Suppress with: + +```tsx +useSWR(key, fetcher, { revalidateOnMount: false }) +``` + +Or use `useSWRImmutable` for static data. + +### Key serialization for SSR fallback + +Use `unstable_serialize` to match keys in fallback maps: + +```tsx +import { unstable_serialize } from 'swr' +import { unstable_serialize as unstable_serialize_infinite } from 'swr/infinite' + +const fallback = { + [unstable_serialize('/api/user')]: userData, + [unstable_serialize_infinite((i) => `/api/posts?page=${i}`)]: [postsData], +} +``` + +## TypeScript Issues + +### Typing the fetcher + +```tsx +// Typed fetcher function +const fetcher = (url: string): Promise => fetch(url).then(r => r.json()) + +// Or type the hook +const { data } = useSWR('/api/user', fetcher) +``` + +### Typing useSWRMutation + +```tsx +const { trigger } = useSWRMutation< + Todo, // Return type + Error, // Error type + string, // Key type + { text: string } // Arg type +>('/api/todos', createTodo) +``` + +### Typing useSWRInfinite + +```tsx +const { data } = useSWRInfinite( + (index, prev) => prev && !prev.length ? null : `/api/posts?page=${index}`, + fetcher +) +// data is Post[][] | undefined +``` + +## Performance Issues + +### Too many rerenders + +SWR only triggers rerenders for state properties you access (tracked via Proxy). If you see excessive rerenders: + +1. Only destructure what you need: `const { data } = useSWR(...)` won't rerender on `isValidating` changes +2. Use `compare` option for custom equality checks +3. Ensure keys are stable + +### Large data sets + +For large cached datasets: + +- Use a custom `compare` function to avoid deep equality checks +- Consider a custom cache provider for persistent storage +- Use `keepPreviousData` to avoid layout shifts + +### Deduplication not working + +Ensure keys serialize to the same string. Use `stableHash` behavior: + +- Primitive keys: compared by value +- Object keys: compared by sorted key-value pairs +- Array keys: compared element by element + +## Migration from v1 to v2 + +### Key changes + +| v1 | v2 | +|---|---| +| `useSWR(key, fn, { initialData })` | `useSWR(key, fn, { fallbackData })` | +| `revalidateOnMount` implicit | `revalidateOnMount` explicit | +| `mutate(key, data, shouldRevalidate)` | `mutate(key, data, { revalidate: bool })` | +| `SWRConfig.value.dedupingInterval` | Same, default changed from 2000ms | +| Return: `{ data, error, isValidating }` | Return: `{ data, error, isValidating, isLoading }` | +| Fetcher gets spread args | Fetcher gets single arg or tuple | +| `import { cache }` | `import { useSWRConfig }` then `config.cache` | +| No mutation hook | `useSWRMutation` from `swr/mutation` | +| No subscription hook | `useSWRSubscription` from `swr/subscription` | +| No immutable hook | `useSWRImmutable` from `swr/immutable` | + +### Fetcher argument changes (v1 → v2) + +```tsx +// v1: arguments spread +useSWR(['/api', id], (url, id) => fetch(`${url}/${id}`)) + +// v2: single tuple argument +useSWR(['/api', id], ([url, id]) => fetch(`${url}/${id}`)) +``` + +### initialData → fallbackData + +```tsx +// v1 +useSWR(key, fetcher, { initialData: data }) + +// v2 +useSWR(key, fetcher, { fallbackData: data }) +```