facebook/react

[Compiler Bug]: server components built externally by the compiler error when rendered in app

Opened this issue · 13 comments

What kind of issue is this?

  • React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
  • babel-plugin-react-compiler (build issue installing or using the Babel plugin)
  • eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
  • react-compiler-healthcheck (build issue installing or using the healthcheck script)

Link to repro

https://github.com/DanielOrtel/compiler-bug

Repro steps

I'm building an esm UI library and ran into a weird issue with one component when adding react compiler to the build pipeline(which uses rollup with babel). You can check the reproduction repo above, steps:

  • set node 20.18.0 if not set automatically
  • run yarn install
  • run yarn test-compiler-error // this will build the library and start a bare-bones next app
  • open localhost:3000\
  • app should error out with the following:
TypeError: Cannot read properties of undefined (reading 'H')
    at IconsRoot (../../libraries/ui/.dist/icons-root/index.js:12:72)
    at AsyncLocalStorage.run (node:async_hooks:346:14)
    at stringify (<anonymous>)
    at stringify (<anonymous>)
    at AsyncLocalStorage.run (node:async_hooks:346:14)
    at AsyncResource.runInAsyncScope (node:async_hooks:206:9)
digest: "2235527820"
  15 |       require("next/dist/compiled/react").__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
  16 |     exports.c = function (size) {
> 17 |       var dispatcher = ReactSharedInternals.H; // <-- ReactSharedInternals is undefined here. 

This is a limited snippet of the larger UI library. The error occurs because the file doesn't have the 'use client' directive, but this component should be server-renderable, it's just a barebones div component. If I remove the react-compiler from the build pipeline or if I make this a client component, it works as expected.

Ultimately, I'd expect adding the compiler to the build pipeline of my library to not break components which previously could be server rendered and force the use of client directive when it is unnecessary.

My suggestion, in case compiler-optimized components can't be rendered in server components, would be to have some config option that can be enabled for libraries to check for client directives and only optimize client components. And also to add some form of an error boundary to let consumers know that they're trying to render a compiler-optimized component in a server environment. The current error is just confusing and it took me a while to figure out what was the exact cause of it.

But the ideal scenario would be to not have to worry about in which environment the component is rendered. Sure, the compiler optimizations do nothing on the server, but that means that they could be essentially stubbed and still render as expected. I dislike the idea of having to output separate versions of components that can be potentially server rendered, simply because I'd like the client versions to be optimized by the compiler.

How often does this bug happen?

Every time

What version of React are you using?

19

What version of React Compiler are you using?

19.0.0-beta-df7b47d-20241124

I wonder if it's the same with vercel/next.js#74182
Question number 1: This setup worked fine when i used react 18 as a dependency in nextjs project, i wonder why.
Question number 2: If you don't precompile ui library and enable nextjs's reactCompiler experimental feature it will work, i wonder why.

Try to set react compiler target to react 18, it worked for me, maybe it's a workaround

This is a bad error message from React. The React Compiler is not supported in a React Server environment (e.g. in React Server Components) since memoization would have no effect and is pure overhead. You should compile to two entrypoints: One for the Client with the React Compiler applied and on for the Server (i.e. for the react-server import condition) without the React Compiler applied.

We'll add a better error message similar to what you'd get when import useState in a React Server environment

This is a bad error message from React. The React Compiler is not supported in a React Server environment (e.g. in React Server Components) since memoization would have no effect and is pure overhead. You should compile to two entrypoints: One for the Client with the React Compiler applied and on for the Server (i.e. for the react-server import condition) without the React Compiler applied.

We'll add a better error message similar to what you'd get when import useState in a React Server environment

Thank you very much for your response, what could help even more than a good error message is adding this information to react-compiler docs on the official website.

This is a bad error message from React. The React Compiler is not supported in a React Server environment (e.g. in React Server Components) since memoization would have no effect and is pure overhead. You should compile to two entrypoints: One for the Client with the React Compiler applied and on for the Server (i.e. for the react-server import condition) without the React Compiler applied.

We'll add a better error message similar to what you'd get when import useState in a React Server environment

As you mentioned, we created two build folders: one compiled and the other uncompiled, and configured imports so that import points to the compiled version and react-server points to the uncompiled one without react-compiler.

The issue arose with the context providers wrapping RSC. They are initially imported via react-server and then re-imported via import, resulting in two separate contexts with the same name at runtime, which causes the context to not be found.

thanks @eps1lon, I assumed this was the case. Though all I could find related to documentation of this approach is this RFC. I assume that this is the recommended approach right now and that frameworks like Next are expected to support the react-server import condition?

thanks @eps1lon, I assumed this was the case. Though all I could find related to documentation of this approach is this RFC. I assume that this is the recommended approach right now and that frameworks like Next are expected to support the react-server import condition?

I just don’t quite understand how I can solve this problem. I have a regular UI library that I want to precompile for another project that uses Next.js. When Next.js imports the ThemeProvider from my library to wrap server components at the very top of the React tree, this react-precompiled file is imported using react-server (which is wrong). How can I ensure that all files marked with use client are imported exclusively via the import directive? After all, when creating a library, I have no way of knowing the context in which the file will be used.

Here’s the situation:
• If I precompile a component, its use as a server component becomes impossible.
• If I don’t precompile, I lose the benefits of using the compiler.
• If I create two versions, using them in different contexts (i mean the same code imported in RSC and from client components) results in different imports being used, causing numerous runtime issues.

I assume that this is the recommended approach right now and that frameworks like Next are expected to support the react-server import condition?

That should already be the case with Next.js

When Next.js imports the ThemeProvider from my library to wrap server components at the very top of the React tree, this react-precompiled file is imported using react-server (which is wrong).

Why is that wrong? ThemeProvider sounds like it's using React Context so it should already have a use client at the top to be usable in a Server Component.

I'll double check how the proposed structure integrates with Next.js to confirm these instructions make sense. We've heard that people want a guide how to publish libraries for React so this is another example why we should do this.

Why is that wrong? ThemeProvider sounds like it's using React Context so it should already have a use client at the top to be usable in a Server Component.

I thought I knew what the problem was and even wrote a long comment about it, but later I realized that wasn’t true. I have no idea what the problem is. The issue is that after I started separating the builds, my contexts values are not seen by useContext further down the tree.

This doesn't work: PopConfirmProvider (which wraps context inside) is not visible down the tree. That's very strange because there is nothing that doesn't let this component to be RSC
Image

Image Image

If i add console.log here:

Image

You can find out that this console.log is executed twice:

Image

And these components are from different files, that's why we get the problem that context provider. Contexts are literally not the same.

Can you include a minimal repro? Try to remove as much code as possible until it no longer reproduces.

I have a super minimal example that I uploaded here: https://github.com/uninstallit/basic-rsc

pnpm install
pnpm build
pnpm start

Open browser on http://localhost:4000 and I get Cannot read properties of undefined (reading 'H')

Is there a workaround or hint how to fix?

@eps1lon @poteto
I tried to create an example for you but gave up because our codebase is too large. Maybe you could make a small example of how, in your understanding, a library with precompiled React code should be structured?
We are currently compiling two folders:
• The dist folder is compiled using tsc and contains completely uncompiled code.
• The compiled folder is compiled using Babel CLI and contains compiled React code.

Then we define the import order via exports:

".": {
"import": "./compiled/index.js",
"types": "./dist/index.d.ts",
"react-server": "./dist/index.js"
}

If this is incorrect, please let us know how it should be done properly.
This doesn’t work because some of the client-executed code (for example, the createContext call) is imported twice: once through import and a second time through react-server, resulting in two different contexts with the same name. As a result, useContext doesn’t recognize the context, and our application doesn’t work.

@DanielOrtel @uninstallit

maybe we can get in contact somehow to figure out together a setup that would work