-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Fix "Purity" hydration error, add random content example #8029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Size changes📦 Next.js Bundle Analysis for react-devThis analysis was generated by the Next.js Bundle Analysis action. 🤖 This PR introduced no changes to the JavaScript bundle! 🙌 |
const [message, setMessage] = useState(''); | ||
|
||
useEffect(() => { | ||
setMessage(messages[Math.floor(messages.length * Math.random())]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
react-hooks/set-state-in-effect
is currently erroring out in the latest workflow run with:
/home/runner/work/react.dev/react.dev/src/content/reference/eslint-plugin-react-hooks/lints/purity.md
91:5 error Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect)
/home/runner/work/react.dev/react.dev/src/content/reference/eslint-plugin-react-hooks/lints/purity.md#codeblock:7:5
5 |
6 | useEffect(() => {
> 7 | setMessage(messages[Math.floor(messages.length * Math.random())]);
| ^^^^^^^^^^ Avoid calling setState() directly within an effect
8 | }, []);
9 |
10 | if (message === '') return; local-rules/lint-markdown-code-blocks
✖ 1 problem (1 error, 0 warnings)
1 error and 0 warnings potentially fixable with the `--fix` option.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
ERROR: "lint" exited with 1.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Error: Process completed with exit code 1.
I could add an eslint-disable-next-line
for react-hooks/set-state-in-effect
, but it seems suboptimal to disable one lint rule to satisfy another.
Three other ways to avoid the reported lint problem, all kind of code-smelly:
useEffect(() => {
startTransition(() => {
setMessage(messages[Math.floor(messages.length * Math.random())]);
});
or:
useEffect(() => {
const id = requestAnimationFrame(() => setMessage(messages[Math.floor(messages.length * Math.random())]));
return () => cancelAnimationFrame(id);
or:
useEffect(() => {
(async () => setMessage(messages[Math.floor(messages.length * Math.random())]))()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way around the react-hooks/set-state-in-effect
problem that @gaearon made me think of in #7012 (comment) and #8033, based on the approach of setting state during rendering mentioned in Adjusting some state when a prop changes:
- useEffect(() => {
- setMessage(messages[Math.floor(messages.length * Math.random())]);
- }, []);
+ if (message === '') {
+ setMessage(messages[Math.floor(messages.length * Math.random())]);
+ }
But then a problem appears for react-hooks/purity
again, because the Math.random()
is in render...
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":
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 Could of course switch to a wrapper function
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 Also, if the Next.js example doesn't show best practice for the React Compiler, should the Next.js docs file |
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
Error Message in Chrome DevTools
The
useState()
docs mention that the initializer function should be pure:Suggested solution
Math.random()
call from theuseState()
initializer function touseEffect()
0
to avoid TypeScript type errors0
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:
message
state variable, initializing with''
useEffect()
''
CodeSandbox: https://codesandbox.io/p/devbox/math-random-without-hydration-error-sjdcmh?file=%2Fapp%2FRandomMessage.tsx%3A7%2C16-17%2C2&workspaceId=ws_GfAuHrswXyA1DoeSwsjjjz
Screen.Recording.2025-10-01.at.11.11.06.mov
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: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 withsuppressHydrationWarning
C) Different approach: using
next/dynamic
withssr: false
(downside: this requires an additional Client Component in between, becausessr: false
cannot be used in thepage.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)