SWR Middleware
Recipe
SWR middleware wraps every useSWR call, letting you inject cross-cutting concerns like logging, authentication headers, request timing, or data transformation without modifying individual hooks.
"use client";
import { Middleware, SWRHook } from "swr";
const logger: Middleware = (useSWRNext: SWRHook) => {
return (key, fetcher, config) => {
const result = useSWRNext(key, fetcher, config);
if (result.error) {
console.error(`[SWR] ${key} error:`, result.error);
} else if (result.data !== undefined) {
console.log(`[SWR] ${key} loaded:`, result.data);
}
return result;
};
};Working Example
"use client";
import useSWR, { SWRConfig, Middleware, SWRHook } from "swr";
// ---- Logging middleware ----
const loggingMiddleware: Middleware = (useSWRNext: SWRHook) => {
return (key, fetcher, config) => {
const start = Date.now();
const result = useSWRNext(key, fetcher, config);
if (!result.isLoading && !result.isValidating) {
console.log(`[SWR] ${key} resolved in ${Date.now() - start}ms`);
}
return result;
};
};
// ---- Auth injection middleware ----
const authMiddleware: Middleware = (useSWRNext: SWRHook) => {
return (key, fetcher, config) => {
const wrappedFetcher = async (...args: any[]) => {
const token = localStorage.getItem("auth_token");
if (!token) throw new Error("Not authenticated");
// If fetcher is a function, call it; otherwise use default
if (typeof fetcher === "function") {
return fetcher(...args);
}
// Default fetch with auth header
const url = typeof key === "string" ? key : (key as any[])[0];
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
};
return useSWRNext(key, wrappedFetcher, config);
};
};
// ---- Data transformation middleware ----
const camelCaseMiddleware: Middleware = (useSWRNext: SWRHook) => {
return (key, fetcher, config) => {
const result = useSWRNext(key, fetcher, config);
return {
...result,
data: result.data ? toCamelCase(result.data) : result.data,
};
};
};
function toCamelCase(obj: any): any {
if (Array.isArray(obj)) return obj.map(toCamelCase);
if (obj && typeof obj === "object") {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [
k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
toCamelCase(v),
])
);
}
return obj;
}
// ---- Apply middleware ----
export default function App({ children }: { children: React.ReactNode }) {
return (
<SWRConfig
value={{
use: [loggingMiddleware, authMiddleware],
fetcher: (url: string) => fetch(url).then((r) => r.json()),
}}
>
{children}
</SWRConfig>
);
}
// ---- Usage (middleware runs automatically) ----
function Dashboard() {
const { data } = useSWR("/api/dashboard");
return <div>{JSON.stringify(data)}</div>;
}Deep Dive
How It Works
- Middleware is an array of functions passed via the
useconfig option, either globally inSWRConfigor per-hook. - Each middleware wraps
useSWRNext, forming a chain similar to Express middleware or Redux middleware. - Middleware executes in order: the first in the array wraps the second, which wraps the third, and so on.
- The innermost call is the actual
useSWRhook logic. - Middleware can modify the key, replace the fetcher, alter config, or transform the returned result.
- Middleware runs on every render, so it must follow React's rules of hooks (no conditional calls).
Variations
Per-hook middleware:
const { data } = useSWR("/api/special", fetcher, {
use: [specialMiddleware],
});Retry with exponential backoff middleware:
const retryMiddleware: Middleware = (useSWRNext) => {
return (key, fetcher, config) => {
return useSWRNext(key, fetcher, {
...config,
onErrorRetry: (error, _key, _config, revalidate, { retryCount }) => {
if (retryCount >= 3) return;
const delay = Math.min(1000 * 2 ** retryCount, 30000);
setTimeout(() => revalidate({ retryCount }), delay);
},
});
};
};Request deduplication tracker:
const dedupTracker: Middleware = (useSWRNext) => {
return (key, fetcher, config) => {
const result = useSWRNext(key, fetcher, config);
if (result.isValidating) {
performance.mark(`swr-start-${key}`);
} else {
performance.mark(`swr-end-${key}`);
performance.measure(`swr-${key}`, `swr-start-${key}`, `swr-end-${key}`);
}
return result;
};
};TypeScript Notes
- Import
MiddlewareandSWRHookfromswrfor proper typing. - The middleware function signature is
(useSWRNext: SWRHook) => SWRHook. SWRHookis(key, fetcher, config) => SWRResponse.
import { Middleware, SWRHook, SWRResponse } from "swr";
const typedMiddleware: Middleware = (useSWRNext: SWRHook) => {
return <Data, Error>(
key: string,
fetcher: ((...args: any[]) => Promise<Data>) | null,
config: any
): SWRResponse<Data, Error> => {
return useSWRNext(key, fetcher, config);
};
};Gotchas
- Middleware runs on every render, not just when data changes. Keep middleware lightweight to avoid performance issues.
- Middleware ordering matters. Auth middleware should typically run before logging middleware so that logs reflect authenticated requests.
- Replacing the fetcher in middleware means the original fetcher passed to
useSWRis ignored. Make sure to call the original if you intend to wrap rather than replace it. - Middleware must follow React's rules of hooks. You cannot conditionally call
useSWRNextinside middleware. - Per-hook middleware in the
useoption does not replace global middleware; it extends it. The global middleware runs first, then per-hook middleware. - Transforming
datain middleware creates a new object reference on every render, which can cause unnecessary re-renders. Memoize if possible.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| SWR middleware | Composable, reusable, transparent | Complex type signatures, runs every render |
| Custom fetcher wrapper | Simpler, explicit | Must be applied manually per fetcher |
| Global onSuccess/onError | Built-in, no middleware needed | Limited to success and error events |
| Axios interceptors | Familiar pattern, request/response level | Tied to axios, not SWR-aware |
FAQs
What is SWR middleware and how does it work?
Middleware is an array of functions passed via the use config option. Each wraps useSWRNext, forming a chain (like Express middleware). Middleware can modify the key, replace the fetcher, alter config, or transform the returned result.
How do I apply middleware globally vs per-hook?
- Global: set
use: [middleware1, middleware2]inSWRConfig. - Per-hook: set
use: [specialMiddleware]in the hook's config. - Per-hook middleware extends (does not replace) global middleware.
Does middleware ordering matter?
Yes. The first middleware in the array wraps the second, which wraps the third, and so on. Auth middleware should typically run before logging middleware so that logs reflect authenticated requests.
Gotcha: Can I conditionally call useSWRNext inside middleware?
No. Middleware must follow React's rules of hooks. You cannot conditionally call useSWRNext -- it must be called on every render, unconditionally.
How do I replace the fetcher in middleware without losing the original?
Wrap the original fetcher rather than replacing it:
const authMiddleware: Middleware = (useSWRNext) => {
return (key, fetcher, config) => {
const wrappedFetcher = async (...args: any[]) => {
// Add auth logic here
if (typeof fetcher === "function") {
return fetcher(...args);
}
};
return useSWRNext(key, wrappedFetcher, config);
};
};Gotcha: Does transforming data in middleware cause unnecessary re-renders?
Yes. Transforming data creates a new object reference on every render, which can trigger re-renders in consuming components. Memoize the transformation if possible to maintain referential stability.
How do I type a middleware function in TypeScript?
import { Middleware, SWRHook } from "swr";
const myMiddleware: Middleware = (useSWRNext: SWRHook) => {
return (key, fetcher, config) => {
return useSWRNext(key, fetcher, config);
};
};The middleware signature is (useSWRNext: SWRHook) => SWRHook.
What TypeScript types should I import for writing SWR middleware?
Import Middleware, SWRHook, and optionally SWRResponse from swr. Use these types rather than writing the full function signature manually, as the signatures are complex.
How often does middleware run?
Middleware runs on every render, not just when data changes. Keep middleware lightweight and avoid expensive operations to prevent performance degradation.
Can middleware modify the SWR config (like adding error retry logic)?
Yes. Spread the existing config and override specific options:
const retryMiddleware: Middleware = (useSWRNext) => {
return (key, fetcher, config) => {
return useSWRNext(key, fetcher, {
...config,
errorRetryCount: 3,
});
};
};