Middleware
Purpose and Scope
Middleware is SWR's extension point for wrapping the behavior of useSWR without changing every component that calls it. A middleware receives the next SWR hook implementation, returns a hook-shaped function, and can inspect or transform the key, fetcher, and configuration before delegating to the next layer. The test suite demonstrates this pattern with logger middleware that records the key and then calls useSWRNext(k, fn, config), preserving the normal SWR request lifecycle while adding cross-cutting behavior around it.
This page is a reference for authors who want to build reusable wrappers for logging, analytics, authentication conventions, request tracing, custom fetcher behavior, or shared domain-specific policies. Middleware is different from a custom hook: a custom hook hides a particular data requirement behind a domain API, while middleware wraps the generic SWR hook pipeline itself. The official API-hooks example describes custom hooks as a way to use SWR internally for different data requirements; middleware is the lower-level option when the concern should apply to many hooks consistently.
Sources: test/use-swr-middlewares.test.tsx, src/_internal/types.ts
Relevant Source Files
test/use-swr-middlewares.test.tsx- Exercises middleware as a public behavior: per-hookuse, globalSWRConfiguse, composition across nested providers, original key forwarding, null fetcher forwarding, and execution order.src/_internal/types.ts- Defines the shared TypeScript contracts that middleware participates in, including fetcher response shapes, fetcher typing, key-sensitive fetcher inference, configuration-related types, and internal state structures used by SWR.
The tests are the most practical source for understanding middleware because they show the shape developers actually write. Each example assigns a function to Middleware, receives useSWRNext, returns another function with (k, fn, config), performs some side effect, and delegates. That is the core contract: middleware should normally call the next hook with a key, a fetcher, and a config object so the rest of SWR can keep handling cache lookup, request deduplication, revalidation, loading state, and response publication.
The type module matters because middleware is not an isolated plugin system. It sits in the same typed ecosystem as SWR keys, fetchers, responses, and configuration. The supplied type definitions show that fetchers return FetcherResponse<Data>, which can be either data or a promise, and that Fetcher<Data, SWRKey> is inferred from the key shape. A middleware that forwards or replaces the fetcher should preserve those expectations so callers still get the same data, error, and loading semantics that useSWR normally provides.
Sources: test/use-swr-middlewares.test.tsx, src/_internal/types.ts
Public Contract
The practical middleware signature is: a Middleware is a function that accepts the next SWR hook and returns another hook-compatible function. The returned function receives the original key argument, the fetcher argument, and the resolved configuration for that hook call. The middleware test imports Middleware from swr, proving it is part of the public package surface rather than only an internal helper. The same test imports withMiddleware from swr/_internal, which is useful for validating behavior but should be treated as internal infrastructure rather than the preferred application API.
import type { Middleware } from 'swr'
const loggerMiddleware: Middleware = useSWRNext => (key, fetcher, config) => {
console.log(key)
return useSWRNext(key, fetcher, config)
}Middleware is enabled through the use option. The tests show two supported placement points. A component can pass middleware directly in a hook-level config object: useSWR(key, fetcher, { use: [loggerMiddleware] }). An application or subtree can also pass middleware through SWRConfig by providing a value with use: [loggerMiddleware]. Both forms produce the same style of wrapper around the hook call, but their scope differs: hook-level middleware affects a single call, while provider middleware applies to all descendant calls that inherit that config.
The returned wrapper receives the original key value, not only SWR's serialized cache key. This is important for middleware that needs to reason about tuple keys, object-like keys, or domain-specific key conventions. The tests call useSWR([key, 1, 2, 3], ...) and assert that middleware sees the same array. Middleware authors should therefore treat the key as the application-level input and avoid assuming that it is always a URL string. Serialization and cache identity are handled deeper in SWR; middleware is allowed to observe the caller's key shape.
Sources: test/use-swr-middlewares.test.tsx, src/_internal/types.ts
Composition and Ordering
Middleware composes as a chain. Each middleware receives a useSWRNext function that already represents the rest of the chain, so the wrapper can run code before calling it, after calling it, or both. The tests demonstrate a pre-call logger that records an identifier and key before delegation. Because React renders before and after data becomes available, the logging middleware is invoked once for the initial render and again when data is ready. Middleware authors should design side effects with render frequency in mind and avoid assuming a single invocation per request.
Nested configuration extends the middleware list rather than replacing all parent behavior. The test for extending middleware renders nested SWRConfig providers and a hook-level use array. The observed order is outer provider middleware, inner provider middleware, and then hook-level middleware, repeated for the initial render and the data-ready render. That means broad application policies can live near the root while more specific policies can be layered closer to a component. The result is predictable composition instead of a hidden global singleton.
Order matters because each middleware can alter what the next middleware sees. A logging layer placed before an authentication layer sees the original fetcher and config; a logging layer after that authentication layer might see a wrapped fetcher or additional options. For cross-cutting middleware, prefer a small, explicit chain where each layer has one responsibility. If a middleware mutates config, document whether downstream middleware should observe that mutation, and prefer returning delegated results unchanged unless the middleware is intentionally changing the SWR response contract.
Sources: test/use-swr-middlewares.test.tsx
Keys, Fetchers, and Configuration
Middleware receives the fetcher exactly as supplied to the hook, including null. The tests verify that when useSWR(key, null, { use: [loggerMiddleware] }) is called, middleware observes null as the fetcher. This is a useful edge case: some SWR calls rely on a global fetcher from configuration, some are conditionally disabled, and some intentionally pass no local fetcher. Middleware that wraps fetchers must first check whether a fetcher exists before calling or decorating it.
The fetcher type definitions explain why preserving fetcher shape matters. FetcherResponse<Data> allows either a synchronous value or a promise, and BareFetcher<Data> accepts arbitrary arguments. The more precise Fetcher<Data, SWRKey> uses the key type to infer the fetcher argument: a function key can produce an argument, falsy keys imply the fetcher is never called, and other key shapes flow through as the fetcher argument. Middleware that replaces a fetcher should maintain this relationship or it can break TypeScript inference and runtime expectations for consumers.
Configuration is the third argument passed through middleware. It is where use itself lives, but it also carries SWR options such as callbacks, fallback data, revalidation controls, and suspense-related behavior. Middleware can read config to adapt behavior, but should be conservative about writing to it. A safe middleware can create a shallow copy when it needs to add or override one option, then pass that copy to useSWRNext. This avoids surprising sibling middleware and keeps the caller's config object from becoming a shared mutable channel.
Sources: test/use-swr-middlewares.test.tsx, src/_internal/types.ts
Reference Patterns
Use per-hook middleware when a behavior belongs to one data dependency. This keeps the extension local and easy to audit. For example, a page might log only one expensive resource, or a component might add a request-specific wrapper around a fetcher. The per-hook use array is also helpful in tests because the component under test declares all of the behavior needed to reproduce the hook call without relying on application-level provider setup.
const loggerMiddleware: Middleware = useSWRNext => (key, fetcher, config) => {
console.log('swr key', key)
return useSWRNext(key, fetcher, config)
}
function Page() {
const { data } = useSWR('/api/user', fetcher, {
use: [loggerMiddleware]
})
return <div>{data ? data.name : 'loading...'}</div>
}Use provider middleware when the behavior is a shared policy. The tests show SWRConfig receiving value={{ use: [loggerMiddleware] }} and applying it to a child hook that does not pass its own middleware. This is the right level for observability, common authorization assumptions, or environment-specific behavior such as development diagnostics. Because nested providers extend the chain, a root provider can define organization-wide middleware while a feature area adds a narrower middleware layer.
<SWRConfig value={{ use: [loggerMiddleware] }}>
<App />
</SWRConfig>A robust middleware should preserve three things unless it has a clear reason not to. First, pass the original key through so SWR can continue to serialize and cache consistently. Second, pass null fetchers through unchanged unless the middleware is intentionally supplying a fetcher from elsewhere. Third, return the result of useSWRNext so consumers receive the standard SWR response object. These conventions let middleware add behavior without becoming a fork of the core hook implementation.
Sources: test/use-swr-middlewares.test.tsx, src/_internal/types.ts
Testing Signals and Next Steps
The middleware test suite gives concrete signals for what downstream code should rely on. Middleware is invoked during render, sees the original key, sees a null fetcher when one is provided, can be installed through context, and composes across nested context and per-hook config. It also demonstrates that render-driven invocation counts can exceed one, so tests for middleware side effects should assert meaningful order or arguments rather than assuming a single call. When writing application middleware, mirror these cases with small focused tests around your own wrapper behavior.
For next steps, read the pages on useSWR, SWRConfig, and TypeScript types together with this one. Middleware is easiest to reason about after you understand the base hook's key, fetcher, response, and configuration contracts. If your goal is to model a business API, start with custom hooks and use middleware only for behavior that truly cuts across many hooks. If your goal is global policy, install middleware in SWRConfig and keep each layer small enough that composition order remains obvious.
Sources: test/use-swr-middlewares.test.tsx, src/_internal/types.ts