Skip to content
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

Fix react-compiler entrypoint for react-server #29057

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

lubieowoce
Copy link
Contributor

@lubieowoce lubieowoce commented May 15, 2024

Summary

Currently, if react/compiler-runtime is imported under "react-server" and c() (a.k.auseMemoCache) is called, it'll crash with "Cannot read properties of undefined (reading 'H')".
Instead, we'd likely want it to 1. work correctly or 2. print a sensible error message.

This happens because the current react-compiler-runtime.{development,production}.js ends up trying to read __CLIENT_INTERNALS, which obviously doesn't exist in react.react-server.

The fix is to add a new react-compiler-runtime.react-server bundle, built with the "react-server" condition, which will make imports of shared/ReactSharedInternals resolve to the server internals instead.

How did you test this change?

Not sure what the best way to test this is, but the following command previously crashed with "Cannot read properties of undefined (reading 'H')". after the change it properly warns about a missing hook call (because i'm calling it outside of render)

$ yarn build; rm -rf node_modules/react
$ NODE_PATH="$PWD/build/oss-stable" node --conditions=react-server -p \
  'require("react/compiler-runtime").c()'

Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app

Adds a `react-compiler-runtime.react-server` entrypoint.
We need a separate "react-server" build, because otherwise
the compiler runtime attempts to use `__CLIENT_INTERNALS` even under the "react-server" condition.

AFAICT this is because in a server environment, we need `shared/ReactSharedInternals` to be rewritten,
but unless a "react-server" condition is passed to the build, it'll default to `ReactSharedInternalsClient`.
@react-sizebot
Copy link

Comparing: 26f2496...153ca6f

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.66 kB 6.66 kB = 1.82 kB 1.82 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 495.02 kB 495.02 kB = 88.68 kB 88.68 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.67 kB 6.67 kB = 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 499.81 kB 499.81 kB = 89.36 kB 89.36 kB
facebook-www/ReactDOM-prod.classic.js = 592.16 kB 592.16 kB = 104.15 kB 104.15 kB
facebook-www/ReactDOM-prod.modern.js = 568.39 kB 568.39 kB = 100.55 kB 100.55 kB
oss-experimental/react/cjs/react-compiler-runtime.react-server.development.js +∞% 0.00 kB 2.92 kB +∞% 0.00 kB 1.44 kB
oss-experimental/react/cjs/react-compiler-runtime.react-server.production.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-experimental/react/cjs/react-compiler-runtime.react-server.profiling.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-experimental/react/compiler-runtime.react-server.js +∞% 0.00 kB 0.44 kB +∞% 0.00 kB 0.27 kB
oss-stable-semver/react/cjs/react-compiler-runtime.react-server.development.js +∞% 0.00 kB 2.92 kB +∞% 0.00 kB 1.44 kB
oss-stable-semver/react/cjs/react-compiler-runtime.react-server.production.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-stable-semver/react/cjs/react-compiler-runtime.react-server.profiling.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-stable-semver/react/compiler-runtime.react-server.js +∞% 0.00 kB 0.44 kB +∞% 0.00 kB 0.27 kB
oss-stable/react/cjs/react-compiler-runtime.react-server.development.js +∞% 0.00 kB 2.92 kB +∞% 0.00 kB 1.44 kB
oss-stable/react/cjs/react-compiler-runtime.react-server.production.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-stable/react/cjs/react-compiler-runtime.react-server.profiling.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-stable/react/compiler-runtime.react-server.js +∞% 0.00 kB 0.44 kB +∞% 0.00 kB 0.27 kB
test_utils/ReactAllWarnings.js Deleted 64.26 kB 0.00 kB Deleted 16.02 kB 0.00 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react/cjs/react-compiler-runtime.react-server.development.js +∞% 0.00 kB 2.92 kB +∞% 0.00 kB 1.44 kB
oss-experimental/react/cjs/react-compiler-runtime.react-server.production.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-experimental/react/cjs/react-compiler-runtime.react-server.profiling.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-experimental/react/compiler-runtime.react-server.js +∞% 0.00 kB 0.44 kB +∞% 0.00 kB 0.27 kB
oss-stable-semver/react/cjs/react-compiler-runtime.react-server.development.js +∞% 0.00 kB 2.92 kB +∞% 0.00 kB 1.44 kB
oss-stable-semver/react/cjs/react-compiler-runtime.react-server.production.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-stable-semver/react/cjs/react-compiler-runtime.react-server.profiling.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-stable-semver/react/compiler-runtime.react-server.js +∞% 0.00 kB 0.44 kB +∞% 0.00 kB 0.27 kB
oss-stable/react/cjs/react-compiler-runtime.react-server.development.js +∞% 0.00 kB 2.92 kB +∞% 0.00 kB 1.44 kB
oss-stable/react/cjs/react-compiler-runtime.react-server.production.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-stable/react/cjs/react-compiler-runtime.react-server.profiling.js +∞% 0.00 kB 1.05 kB +∞% 0.00 kB 0.60 kB
oss-stable/react/compiler-runtime.react-server.js +∞% 0.00 kB 0.44 kB +∞% 0.00 kB 0.27 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-prod.js +0.22% 318.31 kB 319.00 kB +0.18% 56.23 kB 56.33 kB
test_utils/ReactAllWarnings.js Deleted 64.26 kB 0.00 kB Deleted 16.02 kB 0.00 kB

Generated by 🚫 dangerJS against 153ca6f

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, this could be a stub file like

throw new Error("The React compiler does not support Server Components.");

because AFAIK RSC's are not currently intended to be processed by the compiler at all

But also, the Flight dispatcher does have a stub useMemoCache implementation, so i'm not sure what's the preferred behavior here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But also, the Flight dispatcher does have a stub useMemoCache implementation

We should remove it.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A throwing stub is the way.

https://github.com/facebook/react/blob/main/packages/react-dom/npm/server.react-server.js#L3-L5

This export was previously on the main react runtime and that was the reason we couldn't do that. However, if the React Compiler starts supporting some modes in the RSC mode such as optimizing RSC rendering this wouldn't work again.

We have as a principle that we should follow ESM so that there can be an early compiler error if an ESM export is missing. This can catch errors earlier without having to run some code. Erroring early is more important than error with a nice message. It also provides the option to conditionally check for the existence of a method if you import it in the other way that it doesn't early error. E.g. this is why "react" in an RSC layer doesn't export a dummy useState function.

In this case it doesn't harm adding the throw in the module initializer but we might have to drop that once we start having react-compiler support for RSC things. At the end of the day, whoever is configuring an advanced set up like a compiler isn't our main target for nice error messages so it's not the end of the world.

@lubieowoce
Copy link
Contributor Author

lubieowoce commented May 15, 2024

@sebmarkbage Happy to change it, but i see one argument for making this a noop instead of an error-stub that I think we should consider first

If an NPM package exports a universal (server + client) component, and starts compiling its code with the compiler (IIRC shipping compiled code is the recommendation?), then the error-stub would suddenly make importing that component in a server context impossible, because it'd instantly throw. Fixing the error would then require one of these:

  1. The package author needs to mark all the compiled components as client, as they're no longer server-compatible, for no reason other than having used the compiler (people will DEFINITELY forget this)

  2. The package author needs to fork their code based on "react-server" and only compile the client part. This complicates their build process -- code that was previously universal now needs to be forked, and the author needs to understand that + use tooling capable of building the package like that, which is an added ecosystem burden.

  3. The package consumer would need to add to wrap the component in a
    "use client"; export { PreviouslyUniversal } from "compiled-package"
    wrapper to stop the errors. This may not be intuitive, and could lead to client-ifying the code unnecessarily

So if people aren't careful, using the compiler in a package can become an accidental breaking change, causing general frustration.

I definitely understand the desire to be strict here, but I feel like this could end up causing unnecessary ecosystem churn. So maybe it's worth being more lenient and no-oping instead, if we can? Or would you consider that papering over a potential bug, and thus undesireable?

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented May 16, 2024

That's expected though because compiled code might not work with RSC. There's no guarantee that the compiled output would run with any kind of noop memoization and it's not prepared for what we have planned for the JSX runtime inside the RSC layer. The "RSC Compiler" would not work for client code.

So by breaking it now we're ensuring that it keeps working in the contexts that it used to work in the future and doesn't just break in a minor.

The solution for a library that wants to use everything is a more complex build set up but that's the case regardless.

@sebmarkbage
Copy link
Collaborator

More than that, I don't think the compiler story for SSR is fully figured out. E.g. we should really have an SSR compiler that can produce more "string concat" style optimizations and we plan on it excluding things like useEffect and inlining useState.

The React Compiler as is now really shouldn't be applied to the SSR layer so if published to npm with the compiled output it really shouldn't be used with SSR. That story doesn't quite make sense yet.

@lubieowoce
Copy link
Contributor Author

lubieowoce commented May 16, 2024

EDIT: disregard this. i thought i found a thing, but i think i misunderstood the compiler code. This doesn't actually do what that comment says, because moduleName seems to always be react/compiler-runtime unless overridden. although maybe that's a bug, or a leftover from before react/compiler-runtime existed.

Just found another issue:

/*
* If an existing import of React exists (ie `import {useMemo} from 'react'`), inject useMemoCache
* into the list of destructured variables.
*/
function addMemoCacheFunctionSpecifierToExistingImport(
program: NodePath<t.Program>,
moduleName: string,
identifierName: string
): boolean {
let didInsertUseMemoCache = false;
program.traverse({
ImportDeclaration(importDeclPath) {
if (
!didInsertUseMemoCache &&
isNonNamespacedImport(importDeclPath, moduleName)
) {
importDeclPath.pushContainer(
"specifiers",
t.importSpecifier(t.identifier(identifierName), t.identifier("c"))
);
didInsertUseMemoCache = true;
}
},
});
return didInsertUseMemoCache;
}

So:

  • if the file contains import { ... } from 'react', the compiler will add useMemoCache as _c to that
  • otherwise, it'll add a new import { c as _c } from 'react/compiler-runtime'.

So If we turn the server react/compiler-runtime into a throwing stub, we'd be inconsistent: the former case would "work" (because of the dummy useMemoCache in the server dispatcher), but the latter would throw. So I guess we also have to remove useMemoCache from the server entrypoint too? Or maybe we make useMemoCache throw on the server instead, for error message purposes?

@josephsavona
Copy link
Contributor

Thanks for posting this. I think we need to discuss the path forward for React Compiler and SSR/RSC a bit more before proceeding.

josephsavona added a commit that referenced this pull request May 17, 2024
Fixes a stale comment since this caused confusion in #29057 (comment)

[ghstack-poisoned]
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.

None yet

5 participants