Skip to content

Commit ce09ace

Browse files
authored
Improve Error Messages when Access Client References (#26059)
This renames Module References to Client References, since they are in the server->client direction. I also changed the Proxies exposed from the `node-register` loader to provide better error messages. Ideally, some of this should be replicated in the ESM loader too but neither are the source of truth. We'll replicate this in the static form in the Next.js loaders. cc @huozhi @shuding - All references are now functions so that when you call them on the server, we can yield a better error message. - References that are themselves already referring to an export name are now proxies that error when you dot into them. - `use(...)` can now be used on a client reference to unwrap it server side and then pass a reference to the awaited value.
1 parent 78c4bec commit ce09ace

23 files changed

+415
-159
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import type {Thenable} from 'shared/ReactTypes';
1111
import type {LazyComponent} from 'react/src/ReactLazy';
1212

1313
import type {
14-
ModuleReference,
14+
ClientReference,
1515
ModuleMetaData,
1616
UninitializedModel,
1717
Response,
1818
BundlerConfig,
1919
} from './ReactFlightClientHostConfig';
2020

2121
import {
22-
resolveModuleReference,
22+
resolveClientReference,
2323
preloadModule,
2424
requireModule,
2525
parseModel,
@@ -67,7 +67,7 @@ type ResolvedModelChunk<T> = {
6767
};
6868
type ResolvedModuleChunk<T> = {
6969
status: 'resolved_module',
70-
value: ModuleReference<T>,
70+
value: ClientReference<T>,
7171
reason: null,
7272
_response: Response,
7373
then(resolve: (T) => mixed, reject: (mixed) => mixed): void,
@@ -262,7 +262,7 @@ function createResolvedModelChunk<T>(
262262

263263
function createResolvedModuleChunk<T>(
264264
response: Response,
265-
value: ModuleReference<T>,
265+
value: ClientReference<T>,
266266
): ResolvedModuleChunk<T> {
267267
// $FlowFixMe Flow doesn't support functions as constructors
268268
return new Chunk(RESOLVED_MODULE, value, null, response);
@@ -293,7 +293,7 @@ function resolveModelChunk<T>(
293293

294294
function resolveModuleChunk<T>(
295295
chunk: SomeChunk<T>,
296-
value: ModuleReference<T>,
296+
value: ClientReference<T>,
297297
): void {
298298
if (chunk.status !== PENDING && chunk.status !== BLOCKED) {
299299
// We already resolved. We didn't expect to see this.
@@ -589,7 +589,7 @@ export function resolveModule(
589589
const chunks = response._chunks;
590590
const chunk = chunks.get(id);
591591
const moduleMetaData: ModuleMetaData = parseModel(response, model);
592-
const moduleReference = resolveModuleReference(
592+
const moduleReference = resolveClientReference(
593593
response._bundlerConfig,
594594
moduleMetaData,
595595
);

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,16 @@ describe('ReactFlight', () => {
9191
};
9292
});
9393

94-
function moduleReference(value) {
95-
return {
96-
$$typeof: Symbol.for('react.module.reference'),
97-
value: value,
98-
};
94+
function clientReference(value) {
95+
return Object.defineProperties(
96+
function() {
97+
throw new Error('Cannot call a client function from the server.');
98+
},
99+
{
100+
$$typeof: {value: Symbol.for('react.client.reference')},
101+
value: {value: value},
102+
},
103+
);
99104
}
100105

101106
it('can render a Server Component', async () => {
@@ -136,7 +141,7 @@ describe('ReactFlight', () => {
136141
</span>
137142
);
138143
}
139-
const User = moduleReference(UserClient);
144+
const User = clientReference(UserClient);
140145

141146
function Greeting({firstName, lastName}) {
142147
return <User greeting="Hello" name={firstName + ' ' + lastName} />;
@@ -327,7 +332,7 @@ describe('ReactFlight', () => {
327332
return <div>I am client</div>;
328333
}
329334

330-
const ClientComponentReference = moduleReference(ClientComponent);
335+
const ClientComponentReference = clientReference(ClientComponent);
331336

332337
let load = null;
333338
const loadClientComponentReference = () => {
@@ -369,7 +374,7 @@ describe('ReactFlight', () => {
369374
function ClientImpl({children}) {
370375
return children;
371376
}
372-
const Client = moduleReference(ClientImpl);
377+
const Client = clientReference(ClientImpl);
373378

374379
function EventHandlerProp() {
375380
return (
@@ -488,7 +493,7 @@ describe('ReactFlight', () => {
488493
);
489494
}
490495

491-
const ClientComponentReference = moduleReference(ClientComponent);
496+
const ClientComponentReference = clientReference(ClientComponent);
492497

493498
function Server() {
494499
return (
@@ -576,7 +581,7 @@ describe('ReactFlight', () => {
576581
function ClientImpl({value}) {
577582
return <div>{value}</div>;
578583
}
579-
const Client = moduleReference(ClientImpl);
584+
const Client = clientReference(ClientImpl);
580585
expect(() => {
581586
const transport = ReactNoopFlightServer.render(
582587
<Client value={new Date()} />,
@@ -593,7 +598,7 @@ describe('ReactFlight', () => {
593598
function ClientImpl({children}) {
594599
return <div>{children}</div>;
595600
}
596-
const Client = moduleReference(ClientImpl);
601+
const Client = clientReference(ClientImpl);
597602
expect(() => {
598603
const transport = ReactNoopFlightServer.render(
599604
<Client>Current date: {new Date()}</Client>,
@@ -612,7 +617,7 @@ describe('ReactFlight', () => {
612617
function ClientImpl({value}) {
613618
return <div>{value}</div>;
614619
}
615-
const Client = moduleReference(ClientImpl);
620+
const Client = clientReference(ClientImpl);
616621
expect(() => {
617622
const transport = ReactNoopFlightServer.render(<Client value={Math} />);
618623
ReactNoopFlightClient.read(transport);
@@ -629,7 +634,7 @@ describe('ReactFlight', () => {
629634
function ClientImpl({value}) {
630635
return <div>{value}</div>;
631636
}
632-
const Client = moduleReference(ClientImpl);
637+
const Client = clientReference(ClientImpl);
633638
expect(() => {
634639
const transport = ReactNoopFlightServer.render(
635640
<Client value={{[Symbol.iterator]: {}}} />,
@@ -646,7 +651,7 @@ describe('ReactFlight', () => {
646651
function ClientImpl({value}) {
647652
return <div>{value}</div>;
648653
}
649-
const Client = moduleReference(ClientImpl);
654+
const Client = clientReference(ClientImpl);
650655
expect(() => {
651656
const transport = ReactNoopFlightServer.render(
652657
<Client value={{hello: Math, title: <h1>hi</h1>}} />,
@@ -665,7 +670,7 @@ describe('ReactFlight', () => {
665670
function ClientImpl({value}) {
666671
return <div>{value}</div>;
667672
}
668-
const Client = moduleReference(ClientImpl);
673+
const Client = clientReference(ClientImpl);
669674
expect(() => {
670675
const transport = ReactNoopFlightServer.render(
671676
<Client
@@ -702,6 +707,20 @@ describe('ReactFlight', () => {
702707
);
703708
});
704709

710+
it('should warn in DEV if a a client reference is passed to useContext()', () => {
711+
const Context = React.createContext();
712+
const ClientContext = clientReference(Context);
713+
function ServerComponent() {
714+
return React.useContext(ClientContext);
715+
}
716+
expect(() => {
717+
const transport = ReactNoopFlightServer.render(<ServerComponent />);
718+
ReactNoopFlightClient.read(transport);
719+
}).toErrorDev('Cannot read a Client Context from a Server Component.', {
720+
withoutStack: true,
721+
});
722+
});
723+
705724
describe('Hooks', () => {
706725
function DivWithId({children}) {
707726
const id = React.useId();
@@ -776,7 +795,7 @@ describe('ReactFlight', () => {
776795
);
777796
}
778797

779-
const ClientDoublerModuleRef = moduleReference(ClientDoubler);
798+
const ClientDoublerModuleRef = clientReference(ClientDoubler);
780799

781800
const transport = ReactNoopFlightServer.render(<App />);
782801
expect(Scheduler).toHaveYielded([]);
@@ -1000,7 +1019,7 @@ describe('ReactFlight', () => {
10001019
return <span>{context}</span>;
10011020
}
10021021

1003-
const Bar = moduleReference(ClientBar);
1022+
const Bar = clientReference(ClientBar);
10041023

10051024
function Foo() {
10061025
return (
@@ -1077,7 +1096,7 @@ describe('ReactFlight', () => {
10771096
return <div>{value}</div>;
10781097
}
10791098

1080-
const Baz = moduleReference(ClientBaz);
1099+
const Baz = clientReference(ClientBaz);
10811100

10821101
function Bar() {
10831102
return (

packages/react-client/src/forks/ReactFlightClientHostConfig.custom.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ declare var $$$hostConfig: any;
2828
export type Response = any;
2929
export opaque type BundlerConfig = mixed;
3030
export opaque type ModuleMetaData = mixed;
31-
export opaque type ModuleReference<T> = mixed; // eslint-disable-line no-unused-vars
32-
export const resolveModuleReference = $$$hostConfig.resolveModuleReference;
31+
export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
32+
export const resolveClientReference = $$$hostConfig.resolveClientReference;
3333
export const preloadModule = $$$hostConfig.preloadModule;
3434
export const requireModule = $$$hostConfig.requireModule;
3535

packages/react-noop-renderer/src/ReactNoopFlightClient.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type Source = Array<string>;
2222

2323
const {createResponse, processStringChunk, getRoot, close} = ReactFlightClient({
2424
supportsBinaryStreams: false,
25-
resolveModuleReference(bundlerConfig: null, idx: string) {
25+
resolveClientReference(bundlerConfig: null, idx: string) {
2626
return idx;
2727
},
2828
preloadModule(idx: string) {},

packages/react-noop-renderer/src/ReactNoopFlightServer.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ const ReactNoopFlightServer = ReactFlightServer({
4848
clonePrecomputedChunk(chunk: string): string {
4949
return chunk;
5050
},
51-
isModuleReference(reference: Object): boolean {
52-
return reference.$$typeof === Symbol.for('react.module.reference');
51+
isClientReference(reference: Object): boolean {
52+
return reference.$$typeof === Symbol.for('react.client.reference');
5353
},
54-
getModuleKey(reference: Object): Object {
54+
getClientReferenceKey(reference: Object): Object {
5555
return reference;
5656
},
5757
resolveModuleMetaData(

packages/react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {JSResourceReference} from 'JSResourceReference';
1313

1414
import type {ModuleMetaData} from 'ReactFlightDOMRelayClientIntegration';
1515

16-
export type ModuleReference<T> = JSResourceReference<T>;
16+
export type ClientReference<T> = JSResourceReference<T>;
1717

1818
import {
1919
parseModelString,
@@ -25,7 +25,7 @@ export {
2525
requireModule,
2626
} from 'ReactFlightDOMRelayClientIntegration';
2727

28-
import {resolveModuleReference as resolveModuleReferenceImpl} from 'ReactFlightDOMRelayClientIntegration';
28+
import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightDOMRelayClientIntegration';
2929

3030
import isArray from 'shared/isArray';
3131

@@ -37,11 +37,11 @@ export type UninitializedModel = JSONValue;
3737

3838
export type Response = ResponseBase;
3939

40-
export function resolveModuleReference<T>(
40+
export function resolveClientReference<T>(
4141
bundlerConfig: BundlerConfig,
4242
moduleData: ModuleMetaData,
43-
): ModuleReference<T> {
44-
return resolveModuleReferenceImpl(moduleData);
43+
): ClientReference<T> {
44+
return resolveClientReferenceImpl(moduleData);
4545
}
4646

4747
// $FlowFixMe[missing-local-annot]

packages/react-server-dom-relay/src/ReactFlightDOMRelayServerHostConfig.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
1717
import hasOwnProperty from 'shared/hasOwnProperty';
1818
import isArray from 'shared/isArray';
1919

20-
export type ModuleReference<T> = JSResourceReference<T>;
20+
export type ClientReference<T> = JSResourceReference<T>;
2121

2222
import type {
2323
Destination,
@@ -39,21 +39,23 @@ export type {
3939
ModuleMetaData,
4040
} from 'ReactFlightDOMRelayServerIntegration';
4141

42-
export function isModuleReference(reference: Object): boolean {
42+
export function isClientReference(reference: Object): boolean {
4343
return reference instanceof JSResourceReferenceImpl;
4444
}
4545

46-
export type ModuleKey = ModuleReference<any>;
46+
export type ClientReferenceKey = ClientReference<any>;
4747

48-
export function getModuleKey(reference: ModuleReference<any>): ModuleKey {
48+
export function getClientReferenceKey(
49+
reference: ClientReference<any>,
50+
): ClientReferenceKey {
4951
// We use the reference object itself as the key because we assume the
5052
// object will be cached by the bundler runtime.
5153
return reference;
5254
}
5355

5456
export function resolveModuleMetaData<T>(
5557
config: BundlerConfig,
56-
resource: ModuleReference<T>,
58+
resource: ClientReference<T>,
5759
): ModuleMetaData {
5860
return resolveModuleMetaDataImpl(config, resource);
5961
}

packages/react-server-dom-relay/src/__mocks__/ReactFlightDOMRelayClientIntegration.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
1111

1212
const ReactFlightDOMRelayClientIntegration = {
13-
resolveModuleReference(moduleData) {
13+
resolveClientReference(moduleData) {
1414
return new JSResourceReferenceImpl(moduleData);
1515
},
1616
preloadModule(moduleReference) {},

packages/react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ export opaque type ModuleMetaData = {
2929
};
3030

3131
// eslint-disable-next-line no-unused-vars
32-
export opaque type ModuleReference<T> = ModuleMetaData;
32+
export opaque type ClientReference<T> = ModuleMetaData;
3333

34-
export function resolveModuleReference<T>(
34+
export function resolveClientReference<T>(
3535
bundlerConfig: BundlerConfig,
3636
moduleData: ModuleMetaData,
37-
): ModuleReference<T> {
37+
): ClientReference<T> {
3838
if (bundlerConfig) {
3939
const resolvedModuleData = bundlerConfig[moduleData.id][moduleData.name];
4040
if (moduleData.async) {
@@ -64,7 +64,7 @@ function ignoreReject() {
6464
// Start preloading the modules since we might need them soon.
6565
// This function doesn't suspend.
6666
export function preloadModule<T>(
67-
moduleData: ModuleReference<T>,
67+
moduleData: ClientReference<T>,
6868
): null | Thenable<any> {
6969
const chunks = moduleData.chunks;
7070
const promises = [];
@@ -117,7 +117,7 @@ export function preloadModule<T>(
117117

118118
// Actually require the module or suspend if it's not yet ready.
119119
// Increase priority if necessary.
120-
export function requireModule<T>(moduleData: ModuleReference<T>): T {
120+
export function requireModule<T>(moduleData: ClientReference<T>): T {
121121
let moduleExports;
122122
if (moduleData.async) {
123123
// We assume that preloadModule has been called before, which

0 commit comments

Comments
 (0)