Commit 55ac332
authored
feat: lexically scoped React Server Components (#355)
# Lexically Scoped React Server Components
## Summary
This PR introduces **inline `"use client"` and `"use server"`
directives** at the function level, eliminating the requirement that
these directives apply to entire modules. Developers can now define
client components and server functions directly inside any function body
— at arbitrary nesting depths — within a single file.
No other React framework currently supports this capability.
## Motivation
In the standard RSC model, `"use client"` and `"use server"` are
module-level declarations. This forces developers to split
tightly-coupled server and client logic across separate files, leading
to:
- **File proliferation** — a server component with one client button
requires at least two files.
- **Artificial indirection** — the logical unit ("this server page
contains this interactive widget") is scattered across the filesystem.
- **Impossible compositions** — a server function that dynamically
constructs and returns different client components cannot exist, since
they must live in separate modules.
## What's New
### Inline `"use client"` inside server components
```jsx
import { useState } from "react";
function Counter() {
"use client";
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
export default function Page() {
return <Counter />;
}
```
### Inline `"use server"` inside client components
```jsx
"use client";
import { useState } from "react";
export default function LikeButton() {
async function like() {
"use server";
await db.likes.increment();
}
const [liked, setLiked] = useState(false);
return <button onClick={() => { like(); setLiked(true); }}>{liked ? "❤️" : "🤍"}</button>;
}
```
### Arbitrary nesting (server → client → server → …)
Directives can alternate at any depth. A server component can define an
inline client component, which itself defines an inline server function,
and so on.
### Lexical scope capture
Variables from parent scopes are automatically forwarded to extracted
modules:
- **Client components**: captured variables become destructured props,
passed via `createElement` wrappers at the call site.
- **Server functions**: captured variables become
`Function.prototype.bind` arguments prepended to the function
parameters.
### Module state sharing
Top-level declarations (constants, mutable variables, class instances)
are **not** duplicated into extracted modules. Instead, extracted
virtual modules import them from the original module, preserving
identity and mutation semantics.
## Architecture
The implementation consists of three components:
| Component | Role |
|---|---|
| `use-directive-inline.mjs` | Core Vite plugin. Performs AST analysis,
outermost-first multi-pass extraction, virtual module serving, and
source transformation. Generic over directive type. |
| `use-client-inline.mjs` | Configuration for `"use client"`. Defines
how captured variables become destructured props and how call sites
become `createElement` wrappers. |
| `use-server-inline.mjs` | Configuration for `"use server"`. Defines
how captured variables become `.bind()` arguments and how call sites
become direct references. |
The plugin is instantiated once with both configs and registered as a
single Vite plugin (`react-server:use-directive-inline`).
### Extraction algorithm
1. **Parse** the source file and find all functions containing a
directive in their body.
2. **Outermost-first**: only extract the outermost directive functions
across all directive types. Inner directives are handled in subsequent
passes when the extracted virtual module is itself transformed.
3. For each outermost directive function:
- Detect **captured scope variables** (not imports, not top-level
declarations, not the function's own locals).
- Generate a **virtual module** (`file.jsx?use-client-inline=FnName` or
`?use-server-inline=FnName`) containing the extracted function with its
directive, necessary imports, and captured variable injection.
- **Replace** the original function in the source with an import from
the virtual module (or a `createElement` wrapper / `.bind()` call if
captured variables are present).
4. **Remove unused imports** from the original module.
5. **Export top-level declarations** used by extracted functions so
virtual modules can import them.
6. Cache extracted modules for consistent resolution across `resolveId`
/ `load` / `transform` lifecycle.
### Build integration
- The `use-client.mjs` plugin's transform filter was updated to match
query-parameterized module IDs (`/\.m?[jt]sx?(\?.*)?$/`).
- `manifest.mjs` now handles virtual modules with query parameters,
correctly resolving file paths by stripping queries before
`realpathSync` and re-appending them for consistent key generation.
- A second pass in `manifest.mjs` processes `buildClientManifest`
entries that only appear as dynamic imports in the SSR build (inline
`"use client"` modules).
- Both `client.mjs` and `server.mjs` build configurations register the
unified `useDirectiveInline` plugin.1 parent 6e03b14 commit 55ac332
38 files changed
Lines changed: 3658 additions & 332 deletions
File tree
- docs
- messages
- src
- components
- pages
- @sidebar/[[lang]]
- en/(pages)
- advanced
- guide
- ja/(pages)/guide
- packages/react-server/lib
- build
- dev
- plugins
- test
- __test__
- fixtures
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| 28 | + | |
27 | 29 | | |
28 | 30 | | |
| 31 | + | |
29 | 32 | | |
30 | 33 | | |
| 34 | + | |
31 | 35 | | |
32 | 36 | | |
33 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
| 5 | + | |
4 | 6 | | |
5 | 7 | | |
6 | 8 | | |
| |||
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
15 | | - | |
16 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
17 | 23 | | |
18 | 24 | | |
19 | 25 | | |
| |||
25 | 31 | | |
26 | 32 | | |
27 | 33 | | |
| 34 | + | |
28 | 35 | | |
29 | 36 | | |
30 | 37 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
155 | | - | |
| 155 | + | |
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| 25 | + | |
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
| |||
112 | 114 | | |
113 | 115 | | |
114 | 116 | | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
1 | 2 | | |
| 3 | + | |
2 | 4 | | |
3 | 5 | | |
4 | 6 | | |
5 | 7 | | |
6 | 8 | | |
7 | 9 | | |
| 10 | + | |
8 | 11 | | |
9 | 12 | | |
10 | 13 | | |
11 | | - | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
| |||
36 | 39 | | |
37 | 40 | | |
38 | 41 | | |
| 42 | + | |
39 | 43 | | |
40 | 44 | | |
41 | 45 | | |
| |||
77 | 81 | | |
78 | 82 | | |
79 | 83 | | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
114 | 115 | | |
115 | 116 | | |
116 | 117 | | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
117 | 124 | | |
118 | 125 | | |
119 | 126 | | |
| |||
Lines changed: 4 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
45 | 46 | | |
46 | 47 | | |
47 | 48 | | |
48 | | - | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
| |||
0 commit comments