🐛 BUG: useLayoutEffect causes error overlay in dev server
What version of astro are you using?
1.0.0-rc.8
Are you using an SSR adapter? If so, which one?
None
What package manager are you using?
npm
What operating system are you using?
Mac
Describe the Bug
Problem: Using useLayoutEffect in a React component shows the following error in an overlay in dev server:
TypeError: Cannot read properties of null (reading 'useLayoutEffect')
at Proxy.useLayoutEffect (/home/projects/github-nw7vut-bejbg2/node_modules/react/cjs/react.development.js:1637:21)
at __vite_ssr_exports__.default (/src/MyReactComponent.tsx:10:25)
at Tester (@astrojs/react/server.js:38:18)
Recording from `1.0.0-beta.70`
https://user-images.githubusercontent.com/9084735/179028389-ce4926aa-c91e-45f4-92a8-a171c298c9a3.mov
Expectation: I understand that effects cannot run outside the browser, but it should just produce a warning instead of crashing completely. When using external components, it is often not possible to convert those useLayoutEffects into useEffects, so this crash makes it hard to use them. Next.js (which also uses SSG) is able to handle these same components without any crashes.
Workaround: ~~I can avoid the crash using client:only, but obviously this is not ideal, as the markup is not included in the initial response.~~ As of rc.8 the error is exclusive to the dev server, so it doesn't break prod build.
Link to Minimal Reproducible Example
https://stackblitz.com/edit/github-nw7vut-bejbg2?file=src%2FMyReactComponent.tsx
Participation
- [ ] I am willing to submit a pull request for this issue.
Thanks for the report, @mayank99! This is definitely something we need to support, I suspect this is a problem with our React renderer.
I'll take this (so I can be assigned)
Debugging this today, from what I can tell, Vite's SSR seems to be working well, but something's fishy within React that seems to be calling useLayoutEffect twice (once during SSR, twice during stack frame generation to warn about:)
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
I believe this only happens when streaming, hence it only started since beta.55 (https://github.com/withastro/astro/pull/3696). Here are the entire stack trace that shows useLayoutEffect is being called twice:
click to expand
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
at Tester
/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react/cjs/react.development.js:1642
return dispatcher.useLayoutEffect(create, deps);
^
TypeError: Cannot read properties of null (reading 'useLayoutEffect')
at Proxy.useLayoutEffect (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react/cjs/react.development.js:1642:21)
at __vite_ssr_exports__.default (/src/MyReactComponent.tsx:10:25)
at Tester (@astrojs/react/server.js:38:18)
at describeNativeComponentFrame (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:3732:7)
at describeFunctionComponentFrame (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:3827:12)
at getStackByComponentStackNode (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5294:19)
at getCurrentStackInDEV (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5460:12)
at Object.ReactDebugCurrentFrame.getStackAddendum (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react/cjs/react.development.js:130:16)
at printWarning (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:57:40)
at error (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:47:7)
at Object.useLayoutEffect (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5163:5)
at Proxy.useLayoutEffect (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react/cjs/react.development.js:1642:21)
at __vite_ssr_exports__.default (/src/MyReactComponent.tsx:10:25)
at Tester (@astrojs/react/server.js:38:18)
at renderWithHooks (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5662:16)
at renderIndeterminateComponent (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5735:15)
at renderElement (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:5950:7)
at renderNodeDestructiveImpl (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6108:11)
at renderNodeDestructive (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6080:14)
at retryTask (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6532:5)
at performWork (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6580:7)
at /Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6904:12
at scheduleWork (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:78:3)
at startWork (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6903:3)
at renderToNodeStreamImpl (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:7048:3)
at Object.renderToStaticNodeStream (/Users/bjorn/Work/repros/astro-react-ssr/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:7061:10)
at eval (@astrojs/react/server.js:128:46)
at new Promise (<anonymous>)
at renderToStaticNodeStreamAsync (@astrojs/react/server.js:127:9)
at async renderToStaticMarkup (@astrojs/react/server.js:91:11)
at async Object.check (@astrojs/react/server.js:51:2)
at async Module.renderComponent (/node_modules/astro/dist/runtime/server/index.js:182:13)
The stack is operating in the react renderer's check function's Tester component. I'm not quite sure why this happens, but perhaps a way to fix this is to ignore the second error. I also think this might be a bug in React.
I believe this only happens when streaming, hence it only started since
beta.55
Just wanted to mention that it also did happen before beta.55. The difference is that beta.55 shows the error in an overlay whereas beta.70 crashes the dev server.
However, I noticed that rc.7 doesn't crash the dev server anymore (it shows the error overlay, same as beta.55) and it also seems to work fine in prod build, albeit with a "Invalid hook call" warning.
Thanks for checking it @mayank99! Looks like indeed it's working as before now. I think the warnings would be preserved for now so it's expected in this case. I'll go ahead and close this as resolved.
maybe i'm misunderstanding but why is the overlay considered expected behavior? shouldn't it just be a console warning?
The overlay is showed for any errors happening on the server (IIRC), so if we want to hide the overlay, we'd need specific handling for useLayoutEffect. I suppose with React intentionally throwing a non-suppresable error, the goal is to have users to not use useLayoutEffect for SSR, so I think a (perhaps annoying) overlay could be good in the long run.
The overlay is showed for any errors happening on the server (IIRC)
But it's supposed to be a warning, not an error (at least from the wording). And other SSR tools like nextjs handle this just fine. Here's an example with vite-plugin-ssr: https://stackblitz.com/edit/github-fj233q?file=pages%2Findex%2FCounter.jsx
Now if React was throwing an error instead of just logging it in the console, I think the overlay would totally make sense. But that's not the case here.
You're right. Looks like the error is indeed from the second call from https://github.com/withastro/astro/issues/3923#issuecomment-1207832056, not from React. My bad!
In rc.7 we returned to the non-breaking way that doesn't kill the server/build, so I thought this should be fine. But yeah I think it's still nice to investigate this.
I discussed with @matthewp yesterday and it seems like the core issue has something to do with React streaming api throwing errors within itself while it's trying to generate the stack trace for the warning. As you can see from the logs, it has at Tester, but the rest errors out.
I'll re-open this at the meantime until I (or anyone) can repro this to be a pure React streaming bug, or if it's indeed an Astro bug.
Thanks for reopening. I've updated the issue title to more accurately reflect the problem in the latest RC.
Tried to create a React repro today without success. I did notice though, if you export your component as a named function instead like export default function Component() {, the overlay goes away, and the warning only prints in the console (although a few times in a row).
I think this might be the best workaround for now. And it's a better practice too as React Refresh works best with named functions. It's a bit hard for me to invest more time in digging out the React streaming issue. Would this work for your case?