Налаштування 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>
)
}
Нескінченне завантаження
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),
}
}
Умовне отримання даних — залежні запити
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}`)
// дані одразу доступні з 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 дні.







