Настройка SWR для кэширования данных в React-приложении
SWR (stale-while-revalidate) — библиотека от Vercel для получения данных в React. Стратегия проста: сначала возвращает кэшированные данные (stale), параллельно делает запрос (revalidate), обновляет кэш. Пользователь видит данные мгновенно, они тихо обновляются в фоне.
Компактнее React Query, меньше API, оптимальна для проектов на Next.js и там, где не нужны мутации с оптимистичным обновлением и infinite query с пагинацией.
Что входит в работу
Настройка глобального конфига SWR, кастомный fetcher, типизированные хуки, мутации, инвалидация, оффлайн-режим, SSR с Next.js, DevTools.
Установка
npm install swr
Глобальная конфигурация
// main.tsx / _app.tsx
import { SWRConfig } from 'swr'
import { swrFetcher } from '@/lib/fetcher'
function App({ Component, pageProps }: AppProps) {
return (
<SWRConfig
value={{
fetcher: swrFetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
shouldRetryOnError: true,
errorRetryCount: 3,
dedupingInterval: 2000,
onError: (error) => {
if (error.status === 401) {
authStore.logout()
}
},
}}
>
<Component {...pageProps} />
</SWRConfig>
)
}
Fetcher
// lib/fetcher.ts
import type { SWRConfiguration } from 'swr'
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message)
this.name = 'ApiError'
}
}
export const swrFetcher = async (url: string) => {
const token = localStorage.getItem('token')
const res = await fetch(url, {
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
'Content-Type': 'application/json',
},
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new ApiError(res.status, body.message ?? res.statusText)
}
return res.json()
}
Базовые хуки
// hooks/useUser.ts
import useSWR from 'swr'
export function useCurrentUser() {
const { data, error, isLoading, mutate } = useSWR<User>('/api/me')
return {
user: data,
isLoading,
isError: !!error,
error,
revalidate: mutate,
}
}
export function useUser(id: string | null) {
const { data, error, isLoading } = useSWR<User>(
id ? `/api/users/${id}` : null // null — отключает запрос
)
return { user: data, isLoading, isError: !!error }
}
Хук с параметрами
// hooks/useProducts.ts
import useSWR from 'swr'
interface ProductFilters {
categoryId?: string
search?: string
page?: number
sort?: 'price' | 'name' | 'date'
}
export function useProducts(filters: ProductFilters) {
// ключ — URL с параметрами, null отключает запрос
const params = new URLSearchParams(
Object.entries(filters)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)])
)
const { data, error, isLoading, isValidating } = useSWR<PaginatedResponse<Product>>(
`/api/products?${params.toString()}`
)
return {
products: data?.items ?? [],
total: data?.total ?? 0,
isLoading,
isValidating, // true при фоновой ревалидации
isError: !!error,
}
}
Мутации и инвалидация
import useSWR, { useSWRConfig } from 'swr'
function ProductEditor({ id }: { id: string }) {
const { mutate } = useSWRConfig()
const { data: product } = useSWR<Product>(`/api/products/${id}`)
async function handleUpdate(data: UpdateProductDto) {
// оптимистичное обновление
await mutate(
`/api/products/${id}`,
async (current: Product) => {
const updated = await api.patch<Product>(`/products/${id}`, data)
return updated
},
{
optimisticData: (current) => ({ ...current!, ...data }),
rollbackOnError: true,
revalidate: false, // не переспрашиваем после мутации — у нас уже актуальные данные
}
)
// инвалидируем список продуктов
await mutate((key) => typeof key === 'string' && key.startsWith('/api/products?'))
}
// ...
}
useSWRMutation — явные мутации
import useSWRMutation from 'swr/mutation'
async function createProduct(url: string, { arg }: { arg: CreateProductDto }) {
return api.post<Product>(url, arg)
}
function CreateProductForm() {
const { trigger, isMutating, error } = useSWRMutation('/api/products', createProduct)
const { mutate } = useSWRConfig()
async function handleSubmit(data: CreateProductDto) {
const newProduct = await trigger(data)
// инвалидируем все списки
await mutate((key) => typeof key === 'string' && key.includes('/api/products'))
}
return (
<form onSubmit={handleSubmit}>
{/* поля формы */}
<button disabled={isMutating}>
{isMutating ? 'Создание...' : 'Создать'}
</button>
{error && <p className="error">{error.message}</p>}
</form>
)
}
Infinite loading
import useSWRInfinite from 'swr/infinite'
const PAGE_SIZE = 20
function getKey(pageIndex: number, previousPageData: PaginatedResponse<Product> | null) {
if (previousPageData && !previousPageData.hasNextPage) return null
return `/api/products?page=${pageIndex + 1}&pageSize=${PAGE_SIZE}`
}
export function useInfiniteProducts() {
const { data, size, setSize, isLoading, isValidating } = useSWRInfinite<
PaginatedResponse<Product>
>(getKey)
const products = data?.flatMap((page) => page.items) ?? []
const isLoadingMore = isLoading || (size > 0 && data && data[size - 1] === undefined)
const hasMore = data ? data[data.length - 1]?.hasNextPage : true
return {
products,
isLoading,
isLoadingMore,
hasMore,
loadMore: () => setSize(size + 1),
}
}
Conditional fetching — зависимые запросы
function OrderDetails({ orderId }: { orderId: string }) {
const { user } = useCurrentUser()
// запрос выполняется только после получения user
const { data: order } = useSWR<Order>(
user ? `/api/orders/${orderId}` : null
)
// ещё один зависимый запрос
const { data: products } = useSWR<Product[]>(
order?.productIds ? `/api/products?ids=${order.productIds.join(',')}` : null
)
// ...
}
SSR с Next.js (Pages Router)
// pages/products/[id].tsx
import { unstable_serialize } from 'swr'
import { SWRConfig } from 'swr'
export async function getServerSideProps({ params }: GetServerSidePropsContext) {
const product = await fetchProductServer(params!.id as string)
return {
props: {
fallback: {
[unstable_serialize(`/api/products/${params!.id}`)]: product,
},
},
}
}
export default function ProductPage({ fallback }: { fallback: Record<string, Product> }) {
return (
<SWRConfig value={{ fallback }}>
<ProductDetails />
</SWRConfig>
)
}
function ProductDetails() {
const { id } = useRouter().query
const { data } = useSWR<Product>(`/api/products/${id}`)
// data сразу доступна из fallback, без загрузки
return <div>{data?.name}</div>
}
Оффлайн и revalidate on focus
// Глобально отключаем revalidateOnFocus для редко меняющихся данных
const { data } = useSWR('/api/config', fetcher, {
revalidateOnFocus: false,
revalidateIfStale: false, // не переспрашивать, если данные ещё свежие
})
// Ручная инвалидация по событию
window.addEventListener('focus', () => {
mutate('/api/notifications') // обновить только уведомления
})
Структура хуков
src/hooks/
useCurrentUser.ts
useProducts.ts
useProduct.ts
useOrders.ts
useNotifications.ts
Что делаем
Настраиваем глобальный SWRConfig с кастомным fetcher, проектируем хуки под все API-эндпоинты, реализуем оптимистичные мутации для форм, настраиваем инвалидацию при изменениях, при необходимости добавляем SSR-prefetch.
Срок: 2–3 дня.







