Skip to content

Conversation

karlhorky
Copy link
Contributor

@karlhorky karlhorky commented Oct 1, 2025

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

Screenshot 2025-10-01 at 10 47 10

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,

Suggested solution

  1. Move the Math.random() call from the useState() initializer function to useEffect()
  2. Initializing the state variable with 0 to avoid TypeScript type errors
  3. 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

Screenshot 2025-10-01 at 10 58 07

As part of this work, I also added an additional example for random content, which seems like a fairly common use case:

  1. Create a message state variable, initializing with ''
  2. Set a random value of the state variable in useEffect()
  3. 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

Screenshot 2025-10-01 at 11 11 50

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 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)

Copy link

github-actions bot commented Oct 1, 2025

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! 🙌

const [message, setMessage] = useState('');

useEffect(() => {
setMessage(messages[Math.floor(messages.length * Math.random())]);
Copy link
Contributor Author

@karlhorky karlhorky Oct 1, 2025

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())]))()

Copy link
Contributor Author

@karlhorky karlhorky Oct 2, 2025

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...

@karlhorky
Copy link
Contributor Author

karlhorky commented Oct 1, 2025

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":

  1. Arguably is correct behavior, because re-renders of the page (on page load) should cause a random quote to appear
  2. 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:

Screenshot 2025-10-01 at 15 31 26

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant