React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

swrmiddlewareloggingauth

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 use config option, either globally in SWRConfig or 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 useSWR hook 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 Middleware and SWRHook from swr for proper typing.
  • The middleware function signature is (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);
  };
};

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 useSWR is 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 useSWRNext inside middleware.
  • Per-hook middleware in the use option does not replace global middleware; it extends it. The global middleware runs first, then per-hook middleware.
  • Transforming data in middleware creates a new object reference on every render, which can cause unnecessary re-renders. Memoize if possible.

Alternatives

ApproachProsCons
SWR middlewareComposable, reusable, transparentComplex type signatures, runs every render
Custom fetcher wrapperSimpler, explicitMust be applied manually per fetcher
Global onSuccess/onErrorBuilt-in, no middleware neededLimited to success and error events
Axios interceptorsFamiliar pattern, request/response levelTied 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] in SWRConfig.
  • 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,
    });
  };
};