fix(react): guard ThemeProvider default context against undefined document#2478
Conversation
…ument The `context = document?.body` default in ThemeProvider threw `ReferenceError: document is not defined` under SSR (e.g. Next.js App Router prerendering a client component). Optional chaining only guards null/undefined values, not an undeclared global, so reading `document` itself throws first. Use `typeof document !== 'undefined'` to fall back to `undefined` on the server; the component's existing `context?.` guards already handle a missing context. Browser behavior is unchanged. Introduced in #1328, which intended to make ThemeProvider SSR-safe but used optional chaining that never actually protected against the undeclared global. - Add a node-environment regression test (theme.ssr.test.tsx) that renders ThemeProvider via renderToString with no document present. - Guard the DOM shims in setupTests.ts so shared setup runs under the node test environment too.
There was a problem hiding this comment.
Pull request overview
Fixes an SSR crash in the React ThemeProvider default context parameter by avoiding direct evaluation of the document global when it may be undeclared (e.g., Node/SSR runtimes), and adds regression coverage for that scenario.
Changes:
- Updated
ThemeProvider’s defaultcontextto use atypeof document !== 'undefined'guard to preventReferenceErrorunder SSR. - Guarded DOM-dependent clipboard/
execCommandshims in the shared Jest setup so it can run under a Node test environment. - Added a Node-environment SSR test ensuring
renderToString(<ThemeProvider />)does not throw whendocumentis undefined.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/react/src/setupTests.ts | Adds typeof guards so DOM shims don’t execute when navigator/document are absent (node env). |
| packages/react/src/contexts/theme.tsx | Makes the context default SSR-safe by guarding access to document.body. |
| packages/react/src/contexts/theme.ssr.test.tsx | Introduces a node-environment regression test covering the SSR crash scenario. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
rheisler-deque
left a comment
There was a problem hiding this comment.
This PR looks great, nice job testing to make sure the fix works and to protect against regression!
f1ea880
Summary
ThemeProvider's default parametercontext = document?.bodythrowsReferenceError: document is not definedunder any SSR runtime (e.g. Next.js App Router prerendering a"use client"component). Optional chaining only guards against null/undefined values — it does not protect against an undeclared global, so evaluating thedocumentidentifier itself throws before?.ever runs.This makes
<ThemeProvider>(without an explicitcontextprop) unusable in any SSR framework. Passingcontext={null}works around it, confirming the default param is the cause.Root cause / history
Introduced on 2024-01-31 in #1328 — a PR that intended to make
ThemeProviderSSR-safe (document.body→document?.body) but used optional chaining, which never actually protected against the undeclared global. This is a pre-existing bug ondevelop, independent of module format (reproduces on CJS today).Fix
typeofis safe on undeclared globals. In the browser, behavior is identical (document.body); under SSR,contextisundefined, and the component's existingcontext?.guards (effects + MutationObserver) already bail out on a falsy context.Tests
theme.ssr.test.tsxruns under@jest-environment node, wheredocumentgenuinely doesn't exist, and assertsrenderToString(<ThemeProvider …>)doesn't throw. Verified red without the fix (exactReferenceErrorattheme.tsx:32) → green with it. A separate node-env file is used rather than deletingglobal.documentin the jsdom suite, which destabilizes jsdom's async teardown.navigator.clipboard/document.execCommandshims withtypeof … !== 'undefined'so the shared setup also runs under the node environment.Verification
execCommand-dependent suites (CopyButton, Code) plus TreeView still pass — the setup guard preserves jsdom behavior.