Skip to content

[Bug]: next-themes forces stale theme when Next.js cacheComponents/Activity is enabled (React 19, Next 16) #375

@juancarloselorriaga

Description

@juancarloselorriaga

What happened?

Summary

When using next-themes 0.4.6 with Next.js 16 (cacheComponents: true) + React 19, switching locales causes a hidden ThemeProvider to reapply its stale theme when it becomes
active again. The UI and localStorage end up out of sync and the theme flips back to the previous value.

Environment

  • next-themes: 0.4.6
  • Next.js: 16.0.3 (App Router)
  • React: 19.2.0
  • cacheComponents: true (Activity enabled)
  • next-intl: 4.5.3
  • Tailwind: 4.1.x

Repro steps

  1. In next.config.ts, set cacheComponents: true.
  2. Wrap the app with ThemeProvider from next-themes in a Providers component (standard setup: attribute="class", storageKey="my-theme", defaultTheme="system",
    enableSystem, enableColorScheme).
  3. Route structure with locales: app/[locale]/layout.tsx, app/[locale]/page.tsx (or any content).
  4. Use any two locales (e.g., en, es) via next-intl or similar.
  5. Run dev server, open /en.
  6. Sequence:
    • Toggle theme to Dark.
    • Switch locale to ES.
    • Toggle theme to Light.
    • Switch locale back to EN.
  7. Observe: theme flips back to Dark (the stale value). localStorage may show Light, but the DOM class flips to the older value when the hidden tree is reactivated.

Expected

The theme should remain at the last user selection (Light after the sequence), with DOM class and localStorage in sync.

Actual

When returning to the previous locale, the hidden ThemeProvider instance preserved by Activity (cacheComponents) re-applies its stale theme, flipping the UI. Multiple
ThemeProviders appear to mount/unmount; their effects each write to storage/DOM, causing oscillation.

Notes / Hypothesis

  • React 19 + Next 16 Activity keeps previous route trees “hidden” instead of unmounted. next-themes assumes a single live provider; hidden instances still run effects and
    write the theme (DOM class + storage) when they become active again.
  • With cacheComponents disabled (so Activity is off), the bug disappears.
  • A custom single-writer theme manager that applies the class directly and listens to storage events fixes the issue.
  • Mitigations could be:
    • Make next-themes Activity-aware (single-writer or guard DOM/storage writes when hidden).
    • Key ThemeProvider per locale to force unmount on locale change, or provide an option to opt out of reapplying state from hidden trees.
    • Document incompatibility with cacheComponents/Activity for now.

Minimal reproduction (shape)

  • Next.js 16 app with cacheComponents: true
  • app/[locale]/layout.tsx + app/[locale]/page.tsx
  • Providers with next-themes ThemeProvider wrapping the app
  • A simple language switcher using locale routing and a theme switcher calling setTheme('light' | 'dark')
  • Sequence above reliably reproduces in dev

Version

0.4.6

What browsers are you seeing the problem on?

Firefox

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriage

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions