Skip to content

Add first class support for react-server condition #1326

@stipsan

Description

@stipsan

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

  1. Has to use a server only entry point https://github.com/sanity-io/visual-editing/blob/b69c1a50499cce6621a836b1d8a5c7c653f7436c/packages/next-loader/package.json#L18-L24
  2. 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

  1. 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
  2. Need to use bundles: https://github.com/sanity-io/visual-editing/blob/b69c1a50499cce6621a836b1d8a5c7c653f7436c/packages/react-loader/package.config.ts#L6-L11
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions