Fix "Purity" hydration error, add random content example
The last example on the react-hooks/purity rule docs causes a hydration error when SSRed (repro: copy the component into a fresh Next.js app reproduction template and using in the page):
CodeSandbox: https://codesandbox.io/p/devbox/date-now-with-hydration-error-ntchmm?workspaceId=ws_GfAuHrswXyA1DoeSwsjjjz
Show error messages in text
Next.js error message in error overlay
Recoverable Error
Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
See more info here: https://nextjs.org/docs/messages/react-hydration-error
...
<RenderFromTemplateContext>
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<HTTPAccessFallbackBoundary notFound={<SegmentViewNode>} forbidden={undefined} unauthorized={undefined}>
<HTTPAccessFallbackErrorBoundary pathname="/" notFound={<SegmentViewNode>} forbidden={undefined} ...>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
<InnerLayoutRouter url="/" tree={[...]} cacheNode={{lazyData:null, ...}} segmentPath={[...]}>
<SegmentViewNode type="page" pagePath="page.tsx">
<SegmentTrieNode>
<Home>
<Clock>
<div>
+ 1759308311369
- 1759308310748
...
...
...
app/Clock.tsx (16:10) @ Clock
14 | }, []);
15 |
> 16 | return <div>Current time: {time}</div>;
| ^
17 | }
18 |
Error Message in Chrome DevTools
Uncaught Error: Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.
https://react.dev/link/hydration-mismatch
...
<RenderFromTemplateContext>
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<HTTPAccessFallbackBoundary notFound={<SegmentViewNode>} forbidden={undefined} unauthorized={undefined}>
<HTTPAccessFallbackErrorBoundary pathname="/" notFound={<SegmentViewNode>} forbidden={undefined} ...>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
<InnerLayoutRouter url="/" tree={[...]} cacheNode={{lazyData:null, ...}} segmentPath={[...]}>
<SegmentViewNode type="page" pagePath="page.tsx">
<SegmentTrieNode>
<Home>
<Clock>
<div>
+ 1759308311369
- 1759308310748
...
...
...
at throwOnHydrationMismatch (react-dom-client.development.js:4501:11)
at completeWork (react-dom-client.development.js:11899:26)
at runWithFiberInDEV (react-dom-client.development.js:872:30)
at completeUnitOfWork (react-dom-client.development.js:16021:19)
at performUnitOfWork (react-dom-client.development.js:15902:11)
at workLoopConcurrentByScheduler (react-dom-client.development.js:15879:9)
at renderRootConcurrent (react-dom-client.development.js:15854:15)
at performWorkOnRoot (react-dom-client.development.js:15117:13)
at performWorkOnRootViaSchedulerTask (react-dom-client.development.js:16974:7)
at MessagePort.performWorkUntilDeadline (scheduler.development.js:45:48)
The useState() docs mention that the initializer function should be pure:
If you pass a function as initialState, it will be treated as an initializer function. It should be pure,
- https://react.dev/reference/react/useState#parameters
Suggested solution
- Move the
Math.random()call from theuseState()initializer function touseEffect() - Initializing the state variable with
0to avoid TypeScript type errors - Use early return to render nothing if state variable is set to
0
CodeSandbox: https://codesandbox.io/p/devbox/date-now-without-hydration-error-lq5t8c?workspaceId=ws_GfAuHrswXyA1DoeSwsjjjz
As part of this work, I also added an additional example for random content, which seems like a fairly common use case:
- Create a
messagestate variable, initializing with'' - Set a random value of the state variable in
useEffect() - Use early return to render nothing if the state variable is set to
''
CodeSandbox: https://codesandbox.io/p/devbox/math-random-without-hydration-error-sjdcmh?file=%2Fapp%2FRandomMessage.tsx%3A7%2C16-17%2C2&workspaceId=ws_GfAuHrswXyA1DoeSwsjjjz
https://github.com/user-attachments/assets/e416200a-99f8-4d69-9d97-4b613a77422e
cc @poteto @josephsavona
Additional notes
If the change to the Date.now() example is accepted, then it probably should also be changed in the example on this page:
- Components and Hooks must be pure -> Components and Hooks must be idempotent https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent
Alternatives considered
A) Leaving out the early return - this would cause Flash of Unhydrated Content as the page loads, which is probably undesirable (could be changed to a loading message, but that seems out of scope)
B) Disabling hydration warnings on the div element with suppressHydrationWarning
C) Different approach: using next/dynamic with ssr: false (downside: this requires an additional Client Component in between, because ssr: false cannot be used in the page.tsx, because it's a Server Component)
D) Different approach: suggest usage of a Server Component (page), with a call to an impure wrapper function and passing the value to the component (upside: enables random values in SSR)
Size changes
📦 Next.js Bundle Analysis for react-dev
This analysis was generated by the Next.js Bundle Analysis action. 🤖
This PR introduced no changes to the JavaScript bundle! 🙌
Also, if we change focus to Server Components instead:
What's the recommendation for a Server Component (Next.js page) which shows random quotes on every page load?
Currently the example code below "works":
- Arguably is correct behavior, because re-renders of the page (on page load) should cause a random quote to appear
- Is based on the example code from the Next.js
connection()docs by @delbaoliveira (https://github.com/vercel/next.js/blob/canary/docs/01-app/03-api-reference/04-functions/connection.mdx updated in https://github.com/vercel/next.js/pull/70031/files#diff-f2532d57b916ccfb551fd27493164f75eb9a118d1bf0ce874c12f2bebb855838)
app/page.tsx
import { connection } from 'next/server';
const quotes = [
'Unlock diverse expertise',
'Fresh perspectives, proven skills',
'Future-ready talent',
];
export default async function Page() {
await connection();
return <div>{quotes[Math.floor(Math.random() * quotes.length)]}</div>;
}
This currently shows the Cannot call impure function during render error with react-hooks/purity on the Math.random() call:
Could of course switch to a wrapper function mathRandom() which is not recognized by react-hooks/purity, but that seems like just tricking the lint rule - the function is still impure:
app/page.tsx
import { connection } from 'next/server';
const quotes = [
'Unlock diverse expertise',
'Fresh perspectives, proven skills',
'Future-ready talent',
];
function mathRandom() {
return Math.random();
}
export default async function Page() {
await connection();
return <div>{quotes[Math.floor(mathRandom() * quotes.length)]}</div>;
}
What is the recommended pattern for Server Components? Should the react-hooks/purity lint rule be changed to add exceptions for A) usage of async component functions or B) usage of dynamic functions like connection(), headers(), etc?
Also, if the Next.js example doesn't show best practice for the React Compiler, should the Next.js docs file connection.mdx be changed?