Flip the arguments of Blocks and make the query optional (#18374)
* Flip the arguments of Blocks and make the query optional * Rename Query to Load
Sebastian Markbåge committed
Mar 24, 2020 at 10:58 UTC
a317bd033fbb5980f5aa4c6be91fb0040dd37523
7 files changed
+116
-44
packages/react-client/src/ReactFlightClient.js
+1
-1
@@ -152,7 +152,7 @@ export function reportGlobalError(response: Response, error: Error): void {
152
}
153
154
function readMaybeChunk<T>(maybeChunk: Chunk<T> | T): T {
155
- if ((maybeChunk: any).$$typeof !== CHUNK_TYPE) {
155
+ if (maybeChunk == null || (maybeChunk: any).$$typeof !== CHUNK_TYPE) {
156
// $FlowFixMe
157
return maybeChunk;
158
}
packages/react-client/src/__tests__/ReactFlight-test.js
+34
-7
@@ -29,12 +29,15 @@ describe('ReactFlight', () => {
29
act = ReactNoop.act;
30
});
31
32
- function block(query, render) {
32
+ function block(render, load) {
33
return function(...args) {
34
- let curriedQuery = () => {
35
- return query(...args);
34
+ if (load === undefined) {
35
+ return [Symbol.for('react.server.block'), render];
36
+ }
37
+ let curriedLoad = () => {
38
+ return load(...args);
39
};
37
- return [Symbol.for('react.server.block'), render, curriedQuery];
40
+ return [Symbol.for('react.server.block'), render, curriedLoad];
41
};
42
}
43
@@ -70,8 +73,32 @@ describe('ReactFlight', () => {
73
});
74
75
if (ReactFeatureFlags.enableBlocksAPI) {
73
- it('can transfer a Block to the client and render there', () => {
74
- function Query(firstName, lastName) {
76
+ it('can transfer a Block to the client and render there, without data', () => {
77
+ function User(props, data) {
78
+ return (
79
+ <span>
80
+ {props.greeting} {typeof data}
81
+ </span>
82
+ );
83
+ }
84
+ let loadUser = block(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 undefined</span>);
98
+ });
99
+
100
+ it('can transfer a Block to the client and render there, with data', () => {
101
+ function load(firstName, lastName) {
102
return {name: firstName + ' ' + lastName};
103
}
104
function User(props, data) {
@@ -81,7 +108,7 @@ describe('ReactFlight', () => {
108
</span>
109
);
110
}
84
- let loadUser = block(Query, User);
111
+ let loadUser = block(User, load);
112
let model = {
113
User: loadUser('Seb', 'Smith'),
114
};
packages/react-flight-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+9
-6
@@ -44,12 +44,15 @@ describe('ReactFlightDOMRelay', () => {
44
return model;
45
}
46
47
- function block(query, render) {
47
+ function block(render, load) {
48
return function(...args) {
49
- let curriedQuery = () => {
50
- return query(...args);
49
+ if (load === undefined) {
50
+ return [Symbol.for('react.server.block'), render];
51
+ }
52
+ let curriedLoad = () => {
53
+ return load(...args);
54
};
52
- return [Symbol.for('react.server.block'), render, curriedQuery];
55
+ return [Symbol.for('react.server.block'), render, curriedLoad];
56
};
57
}
58
@@ -89,7 +92,7 @@ describe('ReactFlightDOMRelay', () => {
92
});
93
94
it.experimental('can transfer a Block to the client and render there', () => {
92
- function Query(firstName, lastName) {
95
+ function load(firstName, lastName) {
96
return {name: firstName + ' ' + lastName};
97
}
98
function User(props, data) {
@@ -99,7 +102,7 @@ describe('ReactFlightDOMRelay', () => {
102
</span>
103
);
104
}
102
- let loadUser = block(Query, User);
105
+ let loadUser = block(User, load);
106
let model = {
107
User: loadUser('Seb', 'Smith'),
108
};
packages/react-flight-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+10
-7
@@ -62,7 +62,7 @@ describe('ReactFlightDOM', () => {
62
};
63
}
64
65
- function block(query, render) {
65
+ function block(render, load) {
66
let idx = webpackModuleIdx++;
67
webpackModules[idx] = {
68
d: render,
@@ -73,10 +73,13 @@ describe('ReactFlightDOM', () => {
73
name: 'd',
74
};
75
return function(...args) {
76
- let curriedQuery = () => {
77
- return query(...args);
76
+ if (load === undefined) {
77
+ return [Symbol.for('react.server.block'), render];
78
+ }
79
+ let curriedLoad = () => {
80
+ return load(...args);
81
};
79
- return [Symbol.for('react.server.block'), 'path/' + idx, curriedQuery];
82
+ return [Symbol.for('react.server.block'), 'path/' + idx, curriedLoad];
83
};
84
}
85
@@ -288,7 +291,7 @@ describe('ReactFlightDOM', () => {
291
reject(e);
292
};
293
});
291
- function Query() {
294
+ function load() {
295
if (promise) {
296
throw promise;
297
}
@@ -300,8 +303,8 @@ describe('ReactFlightDOM', () => {
303
function DelayedText({children}, data) {
304
return <Text>{children}</Text>;
305
}
303
- let _block = block(Query, DelayedText);
304
- return [_block(), _resolve, _reject];
306
+ let loadBlock = block(DelayedText, load);
307
+ return [loadBlock(), _resolve, _reject];
308
}
309
310
const [FriendsModel, resolveFriendsModel] = makeDelayedText();
packages/react-reconciler/src/__tests__/ReactBlocks-test.js
+31
-9
@@ -49,8 +49,30 @@ describe('ReactBlocks', () => {
49
};
50
});
51
52
+ it.experimental('renders a simple component', () => {
53
+ function User(props, data) {
54
+ return <div>{typeof data}</div>;
55
+ }
56
+
57
+ function App({Component}) {
58
+ return (
59
+ <Suspense fallback={'Loading...'}>
60
+ <Component name="Name" />
61
+ </Suspense>
62
+ );
63
+ }
64
+
65
+ let loadUser = block(User);
66
+
67
+ ReactNoop.act(() => {
68
+ ReactNoop.render(<App Component={loadUser()} />);
69
+ });
70
+
71
+ expect(ReactNoop).toMatchRenderedOutput(<div>undefined</div>);
72
+ });
73
+
74
it.experimental('prints the name of the render function in warnings', () => {
53
- function Query(firstName) {
75
+ function load(firstName) {
76
return {
77
name: firstName,
78
};
@@ -69,7 +91,7 @@ describe('ReactBlocks', () => {
91
);
92
}
93
72
- let loadUser = block(Query, User);
94
+ let loadUser = block(User, load);
95
96
expect(() => {
97
ReactNoop.act(() => {
@@ -86,8 +108,8 @@ describe('ReactBlocks', () => {
108
);
109
});
110
89
- it.experimental('renders a component with a suspending query', async () => {
90
- function Query(id) {
111
+ it.experimental('renders a component with a suspending load', async () => {
112
+ function load(id) {
113
return {
114
id: id,
115
name: readString('Sebastian'),
@@ -102,7 +124,7 @@ describe('ReactBlocks', () => {
124
);
125
}
126
105
- let loadUser = block(Query, Render);
127
+ let loadUser = block(Render, load);
128
129
function App({User}) {
130
return (
@@ -128,7 +150,7 @@ describe('ReactBlocks', () => {
150
it.experimental(
151
'does not support a lazy wrapper around a chunk',
152
async () => {
131
- function Query(id) {
153
+ function load(id) {
154
return {
155
id: id,
156
name: readString('Sebastian'),
@@ -143,7 +165,7 @@ describe('ReactBlocks', () => {
165
);
166
}
167
146
- let loadUser = block(Query, Render);
168
+ let loadUser = block(Render, load);
169
170
function App({User}) {
171
return (
@@ -187,7 +209,7 @@ describe('ReactBlocks', () => {
209
it.experimental(
210
'can receive updated data for the same component',
211
async () => {
190
- function Query(firstName) {
212
+ function load(firstName) {
213
return {
214
name: firstName,
215
};
@@ -203,7 +225,7 @@ describe('ReactBlocks', () => {
225
);
226
}
227
206
- let loadUser = block(Query, Render);
228
+ let loadUser = block(Render, load);
229
230
function App({User}) {
231
return (
packages/react-server/src/ReactFlightServer.js
+6
-6
@@ -191,11 +191,11 @@ export function resolveModelToJSON(
191
}
192
}
193
case '2': {
194
- // Query
195
- let query: () => ReactModel = (value: any);
194
+ // Load function
195
+ let load: () => ReactModel = (value: any);
196
try {
197
- // Attempt to resolve the query.
198
- return query();
197
+ // Attempt to resolve the data.
198
+ return load();
199
} catch (x) {
200
if (
201
typeof x === 'object' &&
@@ -204,12 +204,12 @@ export function resolveModelToJSON(
204
) {
205
// Something suspended, we'll need to create a new segment and resolve it later.
206
request.pendingChunks++;
207
- let newSegment = createSegment(request, query);
207
+ let newSegment = createSegment(request, load);
208
let ping = newSegment.ping;
209
x.then(ping, ping);
210
return serializeIDRef(newSegment.id);
211
} else {
212
- // This query failed, encode the error as a separate row and reference that.
212
+ // This load failed, encode the error as a separate row and reference that.
213
request.pendingChunks++;
214
let errorId = request.nextChunkId++;
215
emitErrorChunk(request, errorId, x);
packages/react/src/ReactBlock.js
+25
-8
@@ -16,14 +16,14 @@ import {
16
REACT_FORWARD_REF_TYPE,
17
} from 'shared/ReactSymbols';
18
19
-type BlockQueryFunction<Args: Iterable<any>, Data> = (...args: Args) => Data;
19
+type BlockLoadFunction<Args: Iterable<any>, Data> = (...args: Args) => Data;
20
export type BlockRenderFunction<Props, Data> = (
21
props: Props,
22
data: Data,
23
) => React$Node;
24
25
type Payload<Props, Args: Iterable<any>, Data> = {
26
- query: BlockQueryFunction<Args, Data>,
26
+ load: BlockLoadFunction<Args, Data>,
27
args: Args,
28
render: BlockRenderFunction<Props, Data>,
29
};
@@ -44,20 +44,20 @@ function lazyInitializer<Props, Args: Iterable<any>, Data>(
44
): BlockComponent<Props, Data> {
45
return {
46
$$typeof: REACT_BLOCK_TYPE,
47
- _data: payload.query.apply(null, payload.args),
47
+ _data: payload.load.apply(null, payload.args),
48
_render: payload.render,
49
};
50
}
51
52
export function block<Args: Iterable<any>, Props, Data>(
53
- query: BlockQueryFunction<Args, Data>,
53
render: BlockRenderFunction<Props, Data>,
54
+ load?: BlockLoadFunction<Args, Data>,
55
): (...args: Args) => Block<Props> {
56
if (__DEV__) {
57
- if (typeof query !== 'function') {
57
+ if (load !== undefined && typeof load !== 'function') {
58
console.error(
59
- 'Blocks require a query function but was given %s.',
60
- query === null ? 'null' : typeof query,
59
+ 'Blocks require a load function, if provided, but was given %s.',
60
+ load === null ? 'null' : typeof load,
61
);
62
}
63
if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
@@ -97,11 +97,28 @@ export function block<Args: Iterable<any>, Props, Data>(
97
}
98
}
99
100
+ if (load === undefined) {
101
+ return function(): Block<Props> {
102
+ let blockComponent: BlockComponent<Props, void> = {
103
+ $$typeof: REACT_BLOCK_TYPE,
104
+ _data: undefined,
105
+ // $FlowFixMe: Data must be void in this scenario.
106
+ _render: render,
107
+ };
108
+
109
+ // $FlowFixMe
110
+ return blockComponent;
111
+ };
112
+ }
113
+
114
+ // Trick to let Flow refine this.
115
+ let loadFn = load;
116
+
117
return function(): Block<Props> {
118
let args: Args = arguments;
119
120
let payload: Payload<Props, Args, Data> = {
104
- query: query,
121
+ load: loadFn,
122
args: args,
123
render: render,
124
};