Skip to content

Commit a56309f

Browse files
authored
[Flight] Integrate Blocks into Flight (#18371)
* Resolve Server-side Blocks instead of Components React elements should no longer be used to extract arbitrary data but only for prerendering trees. Blocks are used to create asynchronous behavior. * Resolve Blocks in the Client * Tests * Bug fix relay JSON traversal It's supposed to pass the original object and not the new one. * Lint * Move Noop Module Test Helpers to top level entry points This module has shared state. It needs to be external from builds. This lets us test the built versions of the Noop renderer.
1 parent fc96a52 commit a56309f

22 files changed

+530
-155
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 114 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,25 @@
77
* @flow
88
*/
99

10-
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
10+
import type {BlockComponent, BlockRenderFunction} from 'react/src/ReactBlock';
11+
import type {LazyComponent} from 'react/src/ReactLazy';
1112

12-
// import type {
13-
// ModuleReference,
14-
// ModuleMetaData,
15-
// } from './ReactFlightClientHostConfig';
13+
import type {
14+
ModuleReference,
15+
ModuleMetaData,
16+
} from './ReactFlightClientHostConfig';
1617

17-
// import {
18-
// resolveModuleReference,
19-
// preloadModule,
20-
// requireModule,
21-
// } from './ReactFlightClientHostConfig';
18+
import {
19+
resolveModuleReference,
20+
preloadModule,
21+
requireModule,
22+
} from './ReactFlightClientHostConfig';
23+
24+
import {
25+
REACT_LAZY_TYPE,
26+
REACT_BLOCK_TYPE,
27+
REACT_ELEMENT_TYPE,
28+
} from 'shared/ReactSymbols';
2229

2330
export type ReactModelRoot<T> = {|
2431
model: T,
@@ -32,40 +39,43 @@ export type JSONValue =
3239
| {[key: string]: JSONValue}
3340
| Array<JSONValue>;
3441

35-
const isArray = Array.isArray;
36-
3742
const PENDING = 0;
3843
const RESOLVED = 1;
3944
const ERRORED = 2;
4045

46+
const CHUNK_TYPE = Symbol('flight.chunk');
47+
4148
type PendingChunk = {|
49+
$$typeof: Symbol,
4250
status: 0,
4351
value: Promise<void>,
4452
resolve: () => void,
4553
|};
46-
type ResolvedChunk = {|
54+
type ResolvedChunk<T> = {|
55+
$$typeof: Symbol,
4756
status: 1,
48-
value: mixed,
57+
value: T,
4958
resolve: null,
5059
|};
5160
type ErroredChunk = {|
61+
$$typeof: Symbol,
5262
status: 2,
5363
value: Error,
5464
resolve: null,
5565
|};
56-
type Chunk = PendingChunk | ResolvedChunk | ErroredChunk;
66+
type Chunk<T> = PendingChunk | ResolvedChunk<T> | ErroredChunk;
5767

5868
export type Response = {
5969
partialRow: string,
6070
modelRoot: ReactModelRoot<any>,
61-
chunks: Map<number, Chunk>,
71+
chunks: Map<number, Chunk<any>>,
6272
};
6373

6474
export function createResponse(): Response {
6575
let modelRoot: ReactModelRoot<any> = ({}: any);
66-
let rootChunk: Chunk = createPendingChunk();
76+
let rootChunk: Chunk<any> = createPendingChunk();
6777
definePendingProperty(modelRoot, 'model', rootChunk);
68-
let chunks: Map<number, Chunk> = new Map();
78+
let chunks: Map<number, Chunk<any>> = new Map();
6979
chunks.set(0, rootChunk);
7080
let response = {
7181
partialRow: '',
@@ -79,6 +89,7 @@ function createPendingChunk(): PendingChunk {
7989
let resolve: () => void = (null: any);
8090
let promise = new Promise(r => (resolve = r));
8191
return {
92+
$$typeof: CHUNK_TYPE,
8293
status: PENDING,
8394
value: promise,
8495
resolve: resolve,
@@ -87,13 +98,14 @@ function createPendingChunk(): PendingChunk {
8798

8899
function createErrorChunk(error: Error): ErroredChunk {
89100
return {
101+
$$typeof: CHUNK_TYPE,
90102
status: ERRORED,
91103
value: error,
92104
resolve: null,
93105
};
94106
}
95107

96-
function triggerErrorOnChunk(chunk: Chunk, error: Error): void {
108+
function triggerErrorOnChunk<T>(chunk: Chunk<T>, error: Error): void {
97109
if (chunk.status !== PENDING) {
98110
// We already resolved. We didn't expect to see this.
99111
return;
@@ -106,21 +118,22 @@ function triggerErrorOnChunk(chunk: Chunk, error: Error): void {
106118
resolve();
107119
}
108120

109-
function createResolvedChunk(value: mixed): ResolvedChunk {
121+
function createResolvedChunk<T>(value: T): ResolvedChunk<T> {
110122
return {
123+
$$typeof: CHUNK_TYPE,
111124
status: RESOLVED,
112125
value: value,
113126
resolve: null,
114127
};
115128
}
116129

117-
function resolveChunk(chunk: Chunk, value: mixed): void {
130+
function resolveChunk<T>(chunk: Chunk<T>, value: T): void {
118131
if (chunk.status !== PENDING) {
119132
// We already resolved. We didn't expect to see this.
120133
return;
121134
}
122135
let resolve = chunk.resolve;
123-
let resolvedChunk: ResolvedChunk = (chunk: any);
136+
let resolvedChunk: ResolvedChunk<T> = (chunk: any);
124137
resolvedChunk.status = RESOLVED;
125138
resolvedChunk.value = value;
126139
resolvedChunk.resolve = null;
@@ -138,10 +151,23 @@ export function reportGlobalError(response: Response, error: Error): void {
138151
});
139152
}
140153

141-
function definePendingProperty(
154+
function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T {
155+
if ((maybeChunk: any).$$typeof !== CHUNK_TYPE) {
156+
// $FlowFixMe
157+
return maybeChunk;
158+
}
159+
let chunk: Chunk<T> = (maybeChunk: any);
160+
if (chunk.status === RESOLVED) {
161+
return chunk.value;
162+
} else {
163+
throw chunk.value;
164+
}
165+
}
166+
167+
function definePendingProperty<T>(
142168
object: Object,
143169
key: string,
144-
chunk: Chunk,
170+
chunk: Chunk<T>,
145171
): void {
146172
Object.defineProperty(object, key, {
147173
configurable: false,
@@ -197,6 +223,55 @@ function createElement(type, key, props): React$Element<any> {
197223
return element;
198224
}
199225

226+
type UninitializedBlockPayload<Data> = [
227+
mixed,
228+
ModuleMetaData | Chunk<ModuleMetaData>,
229+
Data | Chunk<Data>,
230+
];
231+
232+
type Thenable<T> = {
233+
then(resolve: (T) => mixed, reject?: (mixed) => mixed): Thenable<any>,
234+
};
235+
236+
function initializeBlock<Props, Data>(
237+
tuple: UninitializedBlockPayload<Data>,
238+
): BlockComponent<Props, Data> {
239+
// Require module first and then data. The ordering matters.
240+
let moduleMetaData: ModuleMetaData = readMaybeChunk(tuple[1]);
241+
let moduleReference: ModuleReference<
242+
BlockRenderFunction<Props, Data>,
243+
> = resolveModuleReference(moduleMetaData);
244+
// TODO: Do this earlier, as the chunk is resolved.
245+
preloadModule(moduleReference);
246+
247+
let moduleExport = requireModule(moduleReference);
248+
249+
// The ordering here is important because this call might suspend.
250+
// We don't want that to prevent the module graph for being initialized.
251+
let data: Data = readMaybeChunk(tuple[2]);
252+
253+
return {
254+
$$typeof: REACT_BLOCK_TYPE,
255+
_status: -1,
256+
_data: data,
257+
_render: moduleExport,
258+
};
259+
}
260+
261+
function createLazyBlock<Props, Data>(
262+
tuple: UninitializedBlockPayload<Data>,
263+
): LazyComponent<BlockComponent<Props, Data>, UninitializedBlockPayload<Data>> {
264+
let lazyType: LazyComponent<
265+
BlockComponent<Props, Data>,
266+
UninitializedBlockPayload<Data>,
267+
> = {
268+
$$typeof: REACT_LAZY_TYPE,
269+
_payload: tuple,
270+
_init: initializeBlock,
271+
};
272+
return lazyType;
273+
}
274+
200275
export function parseModelFromJSON(
201276
response: Response,
202277
targetObj: Object,
@@ -217,20 +292,26 @@ export function parseModelFromJSON(
217292
if (!chunk) {
218293
chunk = createPendingChunk();
219294
chunks.set(id, chunk);
220-
} else if (chunk.status === RESOLVED) {
221-
return chunk.value;
222295
}
223-
definePendingProperty(targetObj, key, chunk);
224-
return undefined;
296+
return chunk;
225297
}
226298
}
299+
if (value === '@') {
300+
return REACT_BLOCK_TYPE;
301+
}
227302
}
228-
if (isArray(value)) {
303+
if (typeof value === 'object' && value !== null) {
229304
let tuple: [mixed, mixed, mixed, mixed] = (value: any);
230-
if (tuple[0] === REACT_ELEMENT_TYPE) {
231-
// TODO: Consider having React just directly accept these arrays as elements.
232-
// Or even change the ReactElement type to be an array.
233-
return createElement(tuple[1], tuple[2], tuple[3]);
305+
switch (tuple[0]) {
306+
case REACT_ELEMENT_TYPE: {
307+
// TODO: Consider having React just directly accept these arrays as elements.
308+
// Or even change the ReactElement type to be an array.
309+
return createElement(tuple[1], tuple[2], tuple[3]);
310+
}
311+
case REACT_BLOCK_TYPE: {
312+
// TODO: Consider having React just directly accept these arrays as blocks.
313+
return createLazyBlock((tuple: any));
314+
}
234315
}
235316
}
236317
return value;

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

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010

1111
'use strict';
1212

13+
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
14+
15+
let act;
1316
let React;
17+
let ReactNoop;
1418
let ReactNoopFlightServer;
1519
let ReactNoopFlightClient;
1620

@@ -19,24 +23,78 @@ describe('ReactFlight', () => {
1923
jest.resetModules();
2024

2125
React = require('react');
26+
ReactNoop = require('react-noop-renderer');
2227
ReactNoopFlightServer = require('react-noop-renderer/flight-server');
2328
ReactNoopFlightClient = require('react-noop-renderer/flight-client');
29+
act = ReactNoop.act;
2430
});
2531

26-
it('can resolve a model', () => {
32+
function block(query, render) {
33+
return function(...args) {
34+
let curriedQuery = () => {
35+
return query(...args);
36+
};
37+
return [Symbol.for('react.server.block'), render, curriedQuery];
38+
};
39+
}
40+
41+
it('can render a server component', () => {
2742
function Bar({text}) {
2843
return text.toUpperCase();
2944
}
3045
function Foo() {
3146
return {
32-
bar: [<Bar text="a" />, <Bar text="b" />],
47+
bar: (
48+
<div>
49+
<Bar text="a" />, <Bar text="b" />
50+
</div>
51+
),
3352
};
3453
}
3554
let transport = ReactNoopFlightServer.render({
3655
foo: <Foo />,
3756
});
3857
let root = ReactNoopFlightClient.read(transport);
3958
let model = root.model;
40-
expect(model).toEqual({foo: {bar: ['A', 'B']}});
59+
expect(model).toEqual({
60+
foo: {
61+
bar: (
62+
<div>
63+
{'A'}
64+
{', '}
65+
{'B'}
66+
</div>
67+
),
68+
},
69+
});
4170
});
71+
72+
if (ReactFeatureFlags.enableBlocksAPI) {
73+
it('can transfer a Block to the client and render there', () => {
74+
function Query(firstName, lastName) {
75+
return {name: firstName + ' ' + lastName};
76+
}
77+
function User(props, data) {
78+
return (
79+
<span>
80+
{props.greeting}, {data.name}
81+
</span>
82+
);
83+
}
84+
let loadUser = block(Query, User);
85+
let model = {
86+
User: loadUser('Seb', 'Smith'),
87+
};
88+
89+
let transport = ReactNoopFlightServer.render(model);
90+
let root = ReactNoopFlightClient.read(transport);
91+
92+
act(() => {
93+
let UserClient = root.model.User;
94+
ReactNoop.render(<UserClient greeting="Hello" />);
95+
});
96+
97+
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
98+
});
99+
}
42100
});

0 commit comments

Comments
 (0)