Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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;
};
};"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>;
}use config option, either globally in SWRConfig or per-hook.useSWRNext, forming a chain similar to Express middleware or Redux middleware.useSWR hook logic.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;
};
};Middleware and SWRHook from swr for proper typing.(useSWRNext: SWRHook) => SWRHook.SWRHook is (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);
};
};useSWR is ignored. Make sure to call the original if you intend to wrap rather than replace it.useSWRNext inside middleware.use option does not replace global middleware; it extends it. The global middleware runs first, then per-hook middleware.data in middleware creates a new object reference on every render, which can cause unnecessary re-renders. Memoize if possible.| 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 |
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.
use: [middleware1, middleware2] in SWRConfig.use: [specialMiddleware] in the hook's config.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.
No. Middleware must follow React's rules of hooks. You cannot conditionally call useSWRNext -- it must be called on every render, unconditionally.
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);
};
};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.
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.
Import Middleware, SWRHook, and optionally SWRResponse from swr. Use these types rather than writing the full function signature manually, as the signatures are complex.
Middleware runs on every render, not just when data changes. Keep middleware lightweight and avoid expensive operations to prevent performance degradation.
Yes. Spread the existing config and override specific options:
const retryMiddleware: Middleware = (useSWRNext) => {
return (key, fetcher, config) => {
return useSWRNext(key, fetcher, {
...config,
errorRetryCount: 3,
});
};
};Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥