useSWRImmutable

Purpose and Scope

useSWRImmutable is the SWR entrypoint for data that should be fetched through the normal SWR hook contract but should not keep revalidating automatically after it has been cached. It is useful when a resource is effectively static for the lifetime of the page, or when the application wants to control all future updates explicitly rather than through focus, reconnect, polling, or stale-on-mount behavior. The hook keeps the familiar key, fetcher, configuration, and return-value shape of useSWR, but its middleware layer changes the default revalidation posture to immutable semantics.

Sources: src/immutable/index.ts, test/use-swr-immutable.test.tsx

The important distinction is that immutable does not mean the cache can never change. It means the hook disables SWR's automatic revalidation triggers for this hook instance. A cache entry can still be populated by the initial request, read by other mounted consumers, and changed by explicit cache operations elsewhere in an application. The source implements this by wrapping the normal useSWR hook with a middleware rather than by building a separate cache or separate fetching engine. That keeps immutable behavior aligned with the rest of SWR while making the revalidation rules intentionally narrower.

Sources: src/immutable/index.ts

This entrypoint is especially appropriate for versioned assets, build-time metadata, rarely changing feature descriptions, static option lists, or API responses whose freshness is managed outside of the component lifecycle. It is less appropriate for authentication state, dashboards, counters, or collaborative data where focus and reconnect events are expected to repair stale views. When choosing this hook, ask whether the user benefits from SWR's automatic freshness guarantees. If the answer is no, the immutable entrypoint removes unnecessary network work while preserving SWR's ergonomic data fetching model.

Relevant Source Files

  • src/immutable/index.ts - Defines the immutable middleware, applies it with withMiddleware, and exports the default useSWRImmutable hook.
  • immutable/package.json - Provides the package-level entry metadata for the immutable subpath, including CommonJS, ESM, and TypeScript declaration outputs.
  • test/use-swr-immutable.test.tsx - Verifies that the immutable hook and middleware suppress automatic revalidation paths such as remount-driven stale revalidation and focus-triggered revalidation.

Public Entry Point and Imports

Applications import the immutable hook from the immutable subpath. The package shim declares generated distribution files for main, module, and types, so consumers can use the same subpath in JavaScript and TypeScript environments after the package is built. The source file itself imports the base hook from the core index, imports the Middleware type, and imports withMiddleware from the internal layer. This tells readers that immutable is not a parallel implementation; it is a packaged specialization of the normal hook.

Sources: src/immutable/index.ts, immutable/package.json

A typical use looks like this:

import useSWRImmutable from 'swr/immutable'
 
function ProductSchema() {
  const { data, error, isLoading } = useSWRImmutable(
    '/api/schema/product',
    url => fetch(url).then(res => res.json())
  )
 
  if (error) return <p>failed to load schema</p>
  if (isLoading) return <p>loading schema</p>
  return <pre>{JSON.stringify(data, null, 2)}</pre>
}

The hook call is deliberately ordinary: a key identifies the request and the fetcher resolves the data. The difference is in what happens after the first successful population of the cache. With the immutable middleware in place, SWR does not revalidate because another component mounts with the same key, because the browser window regains focus, because the network reconnects, or because a refresh interval is configured. The page still renders through the standard SWR state machine, so loading, data, and error handling can be shared with ordinary useSWR components.

Sources: src/immutable/index.ts, test/use-swr-immutable.test.tsx

Behavior Contract

The immutable middleware always overrides four revalidation-related configuration values before delegating to the next hook implementation. It sets revalidateOnFocus to false, revalidateIfStale to false, revalidateOnReconnect to false, and refreshInterval to zero. Because these assignments happen inside the middleware, user-provided values for those fields are intentionally replaced for this hook path. That is a stronger contract than simply recommending a set of options in documentation; the source makes immutable behavior the final middleware-applied policy for automatic revalidation.

Sources: src/immutable/index.ts

The revalidateIfStale setting is central to the mount behavior. In ordinary SWR usage, a second component mounting with an existing stale cache entry can trigger another fetch. The tests first establish that baseline by using the regular hook, a shared key, a fetcher that increments a value, and a deduping interval of zero. After the first component loads value zero, mounting another component causes the visible value to advance to one. That control case shows the behavior immutable is designed to avoid.

Sources: test/use-swr-immutable.test.tsx

The next test demonstrates that disabling stale revalidation prevents that remount fetch. It uses regular useSWR with revalidateIfStale set to false and confirms that adding the second component does not advance the value. The immutable hook test then repeats the same shape with useSWRImmutable. After the page displays the initial value, the test mounts another consumer, triggers focus, waits, and still expects the value to remain unchanged. Together those cases show that immutable combines the stale-mount suppression with suppression of focus-driven revalidation.

Sources: test/use-swr-immutable.test.tsx

The exported immutable middleware gives advanced users a second composition style. Instead of importing the dedicated hook, a caller can use the regular hook and include immutable in the middleware list. The test suite imports both useSWRImmutable and immutable from the immutable subpath and includes a case for the middleware form. This matters for applications that already have a custom hook stack or middleware pipeline. They can preserve a local hook abstraction while still applying the exact same immutable revalidation policy implemented by the package.

Sources: src/immutable/index.ts, test/use-swr-immutable.test.tsx

API Reference

Default export: useSWRImmutable. This is the preconfigured hook created by applying the immutable middleware to the normal SWR hook. Its practical call signature follows the base SWR hook shape: provide a key, optionally provide a fetcher, and optionally provide configuration. The return object follows normal SWR expectations, including data, error, loading state, and mutation-related capabilities supplied by the base hook. The immutable source does not introduce new return fields, new cache storage, or a separate data model.

Sources: src/immutable/index.ts

Named export: immutable. This is a middleware value. It receives the next SWR hook implementation and returns a hook function that receives key, fetcher, and config. Before passing the call onward, it forces the four revalidation controls to immutable-safe values. Use this export when the application wants to combine immutable behavior with other middleware in a custom configuration. The middleware has the same effect as the default hook for the revalidation fields it controls, so do not expect per-hook config to re-enable those automatic triggers after this middleware runs.

Sources: src/immutable/index.ts, test/use-swr-immutable.test.tsx

Subpath package: swr/immutable. The package metadata for the immutable workspace package points CommonJS consumers at the generated index.js file, ESM consumers at the generated index.mjs file, and TypeScript users at the generated declaration file. The package is private as a workspace package, but it represents the published subpath contents generated into the main package distribution. For day-to-day application code, the practical import path is the subpath used in the tests: import the default hook and, when needed, the immutable middleware from swr/immutable.

Sources: immutable/package.json, test/use-swr-immutable.test.tsx

import useSWR, { SWRConfig } from 'swr'
import useSWRImmutable, { immutable } from 'swr/immutable'
 
useSWRImmutable('/api/static-menu', fetcher)
 
useSWR('/api/static-menu', fetcher, {
  use: [immutable]
})

Implementation Details

The implementation is intentionally small, which is part of the API's design signal. The immutable source imports the Middleware type to describe the exported middleware, imports the base useSWR hook, and imports withMiddleware. The middleware receives useSWRNext, mutates the config object to set the immutable revalidation policy, and returns the result of calling the next hook with the same key, fetcher, and modified config. The default export is then the result of composing the base hook with that middleware.

Sources: src/immutable/index.ts

Because the middleware writes directly to the configuration object it receives, it should be understood as an override layer. A caller might pass revalidateOnFocus true, revalidateOnReconnect true, revalidateIfStale true, or a positive refresh interval, but the immutable middleware changes those values before the request reaches the underlying hook. That behavior is explicit in the source comment stating that all revalidate options are always overridden. The practical result is a predictable immutable profile rather than a helper whose behavior depends on caller option precedence.

Sources: src/immutable/index.ts

The refreshInterval assignment is a useful edge case. Focus, reconnect, and stale-on-mount are event-like triggers, while refresh intervals are time-based polling. Immutable disables both categories. If a component needs polling most of the time but also wants to avoid focus revalidation, it should use regular useSWR with targeted options rather than useSWRImmutable. Conversely, if a component should never poll automatically after the initial load, the immutable hook prevents accidental polling even if a shared configuration provider supplies a refresh interval higher in the tree.

Sources: src/immutable/index.ts

The tests use a zero deduping interval to make revalidation attempts observable. Without that setting, request deduplication could hide whether a second mount initiated a fetch. By forcing the fetcher to increment a local value and by waiting after user events, the tests make the expected difference visible in rendered text. This is a helpful pattern when debugging application code too: if an immutable resource seems to refetch, isolate the key, remove unrelated deduping effects, and verify whether the refetch is automatic or caused by an explicit mutation or key change.

Sources: test/use-swr-immutable.test.tsx

Choosing Immutable Versus Regular SWR

Use regular useSWR when the data should become fresh again as user context changes. The core SWR model is built around returning cached data quickly and then revalidating through events such as focus and network recovery. That model is excellent for user profiles, permissions, notifications, account balances, and other data where the browser returning to the foreground is a meaningful freshness signal. The immutable entrypoint deliberately opts out of those signals, so it should be chosen when the first successful response is good enough until the key changes or the cache is explicitly updated.

Sources: src/immutable/index.ts, test/use-swr-immutable.test.tsx

Use useSWRImmutable when the identity of the key already encodes the version or when the resource is operationally static. Examples include a content hash, a release identifier, a locale bundle version, or an endpoint that serves a fixed configuration for the current deployment. If the key changes, the hook still observes a different request identity through the normal SWR mechanism. Immutable therefore works best with stable, meaningful keys. It is not a replacement for proper cache invalidation when the same key may legitimately produce changing results that users need to see automatically.

Sources: src/immutable/index.ts

Global configuration needs special attention. A surrounding provider can supply fetchers, cache providers, fallback values, callbacks, and revalidation defaults for ordinary hooks. The immutable middleware still overrides its four automatic revalidation fields at the hook level. That means a team can safely place immutable consumers inside an application-wide configuration that enables focus revalidation or polling for most data. The immutable consumers will keep their no-automatic-revalidation policy while continuing to benefit from shared fetcher and cache behavior supplied by the base SWR stack.

Sources: src/immutable/index.ts, test/use-swr-immutable.test.tsx

Testing Signals and Troubleshooting

The immutable tests exercise user-visible outcomes rather than only inspecting options. They render a page, wait for initial data, click a button that mounts another component with the same key, optionally focus the window, and then assert that the displayed data either changes or remains stable. This end-to-end component style is valuable because immutable behavior is about lifecycle events. A static type check can confirm the import, but only a mounted component test can prove that remount and focus paths do not cause the fetcher to run again.

Sources: test/use-swr-immutable.test.tsx

If an immutable hook appears to update, first check whether the key changed. A different key represents a different cache entry and should fetch. Next, check whether some application code called a mutation API or wrote into the cache explicitly. Immutable disables automatic revalidation, not all cache updates. Finally, check whether the component is actually using useSWRImmutable or the immutable middleware from the immutable subpath. Accidentally importing the regular hook with ordinary options can reintroduce stale-on-mount, focus, reconnect, or interval behavior depending on the surrounding configuration.

Sources: src/immutable/index.ts, test/use-swr-immutable.test.tsx

For related reading, start with the main useSWR API to understand the shared hook contract, then review Middleware to see how wrapper functions compose behavior, and Global Configuration to understand which options can be supplied by providers. If the data is not static and instead needs user-triggered or optimistic updates, continue to mutate and useSWRMutation rather than relying on immutable semantics. If the data is paginated, the infinite loading APIs are separate and should be evaluated on their own revalidation needs.