Skip to content

[Bug]: React 19.2 dev error "Encountered a script tag while rendering React component" when ThemeProvider remounts on client (e.g. switching a root dynamic segment like [lang]) #397

Description

@RuixeWolf

What happened?

What happened?

When the ThemeProvider subtree remounts on the client, React 19.2 logs the following console error in development:

Encountered a script tag while rendering React component. Scripts inside React components are never executed when rendering on the client. Consider using template tag instead (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).

    at script (<anonymous>:null:null)
    at ThemeProvider (...next-themes/dist/index.mjs)
    at LocaleLayout (app/[lang]/layout.tsx:185)

Steps to reproduce

  1. Create a Next.js App Router app (Next.js 16, React 19.2.x) with the root layout inside a dynamic segment — e.g. app/[lang]/layout.tsx as required by next-intl's locale-prefixed routing.

  2. Wrap the app in ThemeProvider inside that layout:

    // app/[lang]/layout.tsx
    <html lang={locale}>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
          {children}
        </ThemeProvider>
      </body>
    </html>
  3. Switch locales with a client-side navigation that changes the root segment value, e.g. next-intl's useRouter().push(pathname, { locale }).

  4. Observe the browser console in dev mode — the error above appears immediately after the remount.

Root cause analysis

I dug into this and verified each step in the browser:

  1. Changing a root segment value makes Next.js remount the entire root-layout subtree (not just re-render it). I confirmed this by tagging DOM nodes with expando properties before the navigation: every <script> element inside the ThemeProvider subtree was re-created after the locale switch, while a normal same-locale navigation remounts nothing.

  2. next-themes renders its no-flash inline <script> inside the provider via React.createElement("script", { dangerouslySetInnerHTML: ... }) (the memoized ThemeScript component in dist/index.mjs).

  3. React 19.2's react-dom client renderer now explicitly warns when a client-render-created <script> is not a data block (checked in completeWork: the element is created through a dummy <div> + innerHTML trick, i.e. it is intentionally inert, and console.error fires unless isScriptDataBlock(props) is true — only non-executable types like application/ld+json are exempt; the async-script hoisting path only applies to scripts with a string src, so an inline script can't opt out).

  4. The re-created script never executes — but that's fine, because its only job (applying the theme class before first paint) already ran when the initial SSR HTML loaded, and the provider's useEffect re-applies the theme on remount. So the error is noisy but harmless: theming keeps working. It's purely a DX problem.

Any scenario that remounts the provider on the client should hit this (root segment change is just the most common one with i18n routing).

Expected behavior

No error. The no-flash script only needs to render for SSR + the initial hydration mount. On subsequent client-side remounts it should be skipped entirely.

Suggested fix

Render the script only for the first mount, using a module-level flag flipped in an effect:

let hasMounted = false

const ThemeScript = memo((props) => {
  const contents = ...
  useEffect(() => {
    hasMounted = true
  }, [])
  // On client re-mounts (after first hydration), skip the script: its job is
  // already done and React 19.2 errors on client-rendered script tags.
  return hasMounted ? null : createElement('script', {...})
})
  • During SSR and the first hydration mount, hasMounted is false, so the script renders exactly as today (the useEffect only runs after hydration).
  • Any later client remount renders null — no inert script element, no React warning, identical behavior (theme re-application already happens in the provider's effects).

I've verified this exact patch locally against 0.4.6: the error disappears on locale switches, SSR output is unchanged, and theme toggling/persistence keeps working after remounts. Happy to open a PR if the approach sounds good.

Version

next-themes 0.4.6 (latest) — also relevant: next 16.3.0, react/react-dom 19.2.8

What browsers are you seeing the problem on?

Chrome, Firefox, Microsoft Edge, Safari, other (the error is emitted by React's client renderer in dev mode, so it is browser-independent)

Version

0.4.6

What browsers are you seeing the problem on?

Firefox, Chrome, Safari, Microsoft Edge

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