-
Notifications
You must be signed in to change notification settings - Fork 5
Description
When bundling a react library there are important considerations that must be taken into account:
- frameworks like next.js with react server components support treats components as server components by default, to use client APIs like
useState
, the'use client'
directive must be added. - React Compiler should only run on code that's considered client components, adding it to a server only component would be pure overhead as servers don't rerender with state, only browsers do.
In other words, if a library is already requiring 'use client'
, then we have a simple path for shipping a package that is precompiled with React Compiler, and works when used in stand-alone vite, as well as frameworks like next.js:
package.json
{
"type": "module",
"exports": {
".": {
"source": "./src/index.ts",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
}
}
package.config.ts
import {defineConfig} from '@sanity/pkg-utils'
export default defineConfig({
babel: {reactCompiler: true},
reactCompilerOptions: {target: '18'},
rollup: {
output: {
banner: () => {
return `'use client';`
},
},
},
})
react-rx uses this setup.
1. Use case, allow using React Compiler on libraries that can't require 'use client'
But if a library doesn't, (and shouldn't), require 'use client'
, then we don't currently have a path for using React Compiler there yet.
A good example of that is @portabletext/react
.
If we were to require userland to use 'use client'
, or where to add it ourselves like react-rx
does, then it would disallow using async server components to render rich text (it's nice that code snippets can highlight and color the code all on the server so that the browser doesn't need to download and execute heavy libraries like prismjs).
On the other hand, @portabletext/react
would benefit greatly from React Compiler for apps that render portable text in a client component already, as well as contexts similar to markdown where @portabletext/editor
renders on the left split screen, and a live preview renders on the right side, where there's going to be massive amounts of rerenders client side.
If we add first class support for the react-server
export condition:
{
"type": "module",
"exports": {
".": {
"source": "./src/index.ts",
"react-server": "./dist/index.react-server.js",
"require": "./dist/index.cjs",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
}
}
Then we could ensure that the bundle we make for ./dist/index.react-server.js
isn't using React Compiler.
Frameworks implementing RCS and 'use client'
have to also support the react-server
condition, like Next.js is. That way we can ship @portabletext/react
with React Compiler optimizations that are used when it should be, while server components don't get unnecessary overhead and continue to support using async server components to render custom block types.
2. Use case, make it easier for libraries that need to ship different code for server components and client components
Sometimes a library want to require some exports to be server rendered, and ban client components.
@sanity/next-loader
's defineLive
does this (we expose this API publicly as import {defineLive} from 'next-sanity'
, where next-sanity
has a direct internal dependency on @sanity/next-loader
).
Calling defineLive
from a client component will throw an error, internally it needs to call APIs that Next.js only expose to server components.
Other times it's the other way around, in @sanity/react-loader
, it has a useQuery
export that should never be called by a server component. It's for client components, while RSC's are supposed to use loadQuery
.
Both libraries have counter-intuitive build configs to make this happen today.
@sanity/next-loader
- Has to use a server only entry point https://github.com/sanity-io/visual-editing/blob/b69c1a50499cce6621a836b1d8a5c7c653f7436c/packages/next-loader/package.json#L18-L24
- And then bundle for client components by using the
bundles
config https://github.com/sanity-io/visual-editing/blob/b69c1a50499cce6621a836b1d8a5c7c653f7436c/packages/next-loader/package.config.ts#L17-L22
@sanity/react-loader
- Needs to target browsers, so it's much more complicated: https://github.com/sanity-io/visual-editing/blob/b69c1a50499cce6621a836b1d8a5c7c653f7436c/packages/react-loader/package.json#L18-L33
- Need to use
bundles
: https://github.com/sanity-io/visual-editing/blob/b69c1a50499cce6621a836b1d8a5c7c653f7436c/packages/react-loader/package.config.ts#L6-L11 - The underlying
createQueryStore
has to be declared in multiple ways: https://github.com/sanity-io/visual-editing/tree/main/packages/react-loader/src/createQueryStore
By having the react-server
export condition we could've solved this far more elegantly (and without needing to declare bundles
in package.config.ts
)
{
"type": "module",
"exports": {
".": {
"source": "./src/index.ts",
"react-server": {
"source": "./src/index.react-server.ts",
"default": "./dist/index.react-server.js"
},
"require": "./dist/index.cjs",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
}
}
Other benefits of this condition, beyond knowing the React Compiler should be turned off on that target, is that it never runs in the browser so the browserslist target used for the output syntax only needs to take into consideration the node.js version we support, and other similar optimizations we do when we target node
as the runtime.