[Flight] Support more element types and Hooks for Server and Hybrid Components (#19711)
* Shim support for more element types * Shim commonly used Hooks that are safe * Flow * Oopsie
Dan Abramov committed
Aug 27, 2020 at 20:19 UTC
c1ac052158ee9ec29d3e748420d9d577b8b7412d
3 files changed
+166
-5
packages/react-server/src/ReactFlightServer.js
+75
-4
@@ -7,6 +7,7 @@
7
* @flow
8
*/
9
10
+import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
11
import type {
12
Destination,
13
Chunk,
@@ -29,12 +30,24 @@ import {
30
31
import {
32
REACT_BLOCK_TYPE,
32
- REACT_SERVER_BLOCK_TYPE,
33
REACT_ELEMENT_TYPE,
34
+ REACT_DEBUG_TRACING_MODE_TYPE,
35
+ REACT_FORWARD_REF_TYPE,
36
REACT_FRAGMENT_TYPE,
37
REACT_LAZY_TYPE,
38
+ REACT_LEGACY_HIDDEN_TYPE,
39
+ REACT_MEMO_TYPE,
40
+ REACT_OFFSCREEN_TYPE,
41
+ REACT_PROFILER_TYPE,
42
+ REACT_SCOPE_TYPE,
43
+ REACT_SERVER_BLOCK_TYPE,
44
+ REACT_STRICT_MODE_TYPE,
45
+ REACT_SUSPENSE_TYPE,
46
+ REACT_SUSPENSE_LIST_TYPE,
47
} from 'shared/ReactSymbols';
48
49
+import * as React from 'react';
50
+import ReactSharedInternals from 'shared/ReactSharedInternals';
51
import invariant from 'shared/invariant';
52
53
type ReactJSONValue =
@@ -74,6 +87,8 @@ export type Request = {
87
toJSON: (key: string, value: ReactModel) => ReactJSONValue,
88
};
89
90
+const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
91
+
92
export function createRequest(
93
model: ReactModel,
94
destination: Destination,
@@ -110,11 +125,33 @@ function attemptResolveElement(element: React$Element<any>): ReactModel {
125
return [REACT_ELEMENT_TYPE, type, element.key, element.props];
126
} else if (type[0] === REACT_SERVER_BLOCK_TYPE) {
127
return [REACT_ELEMENT_TYPE, type, element.key, element.props];
113
- } else if (type === REACT_FRAGMENT_TYPE) {
128
+ } else if (
129
+ type === REACT_FRAGMENT_TYPE ||
130
+ type === REACT_STRICT_MODE_TYPE ||
131
+ type === REACT_PROFILER_TYPE ||
132
+ type === REACT_SCOPE_TYPE ||
133
+ type === REACT_DEBUG_TRACING_MODE_TYPE ||
134
+ type === REACT_LEGACY_HIDDEN_TYPE ||
135
+ type === REACT_OFFSCREEN_TYPE ||
136
+ // TODO: These are temporary shims
137
+ // and we'll want a different behavior.
138
+ type === REACT_SUSPENSE_TYPE ||
139
+ type === REACT_SUSPENSE_LIST_TYPE
140
+ ) {
141
return element.props.children;
115
- } else {
116
- invariant(false, 'Unsupported type.');
142
+ } else if (type != null && typeof type === 'object') {
143
+ switch (type.$$typeof) {
144
+ case REACT_FORWARD_REF_TYPE: {
145
+ const render = type.render;
146
+ return render(props, undefined);
147
+ }
148
+ case REACT_MEMO_TYPE: {
149
+ const nextChildren = React.createElement(type.type, element.props);
150
+ return attemptResolveElement(nextChildren);
151
+ }
152
+ }
153
}
154
+ invariant(false, 'Unsupported type.');
155
}
156
157
function pingSegment(request: Request, segment: Segment): void {
@@ -236,9 +273,11 @@ export function resolveModelToJSON(
273
value !== null &&
274
value.$$typeof === REACT_ELEMENT_TYPE
275
) {
276
+ const prevDispatcher = ReactCurrentDispatcher.current;
277
// TODO: Concatenate keys of parents onto children.
278
const element: React$Element<any> = (value: any);
279
try {
280
+ ReactCurrentDispatcher.current = Dispatcher;
281
// Attempt to render the server component.
282
value = attemptResolveElement(element);
283
} catch (x) {
@@ -253,6 +292,8 @@ export function resolveModelToJSON(
292
// Something errored. Don't bother encoding anything up to here.
293
throw x;
294
}
295
+ } finally {
296
+ ReactCurrentDispatcher.current = prevDispatcher;
297
}
298
}
299
@@ -378,3 +419,33 @@ export function startFlowing(request: Request): void {
419
request.flowing = true;
420
flushCompletedChunks(request);
421
}
422
+
423
+function unsupportedHook(): void {
424
+ invariant(false, 'This Hook is not supported in Server Components.');
425
+}
426
+
427
+const Dispatcher: DispatcherType = {
428
+ useMemo<T>(nextCreate: () => T): T {
429
+ return nextCreate();
430
+ },
431
+ useCallback<T>(callback: T): T {
432
+ return callback;
433
+ },
434
+ useDebugValue(): void {},
435
+ useDeferredValue<T>(value: T): T {
436
+ return value;
437
+ },
438
+ useTransition(): [(callback: () => void) => void, boolean] {
439
+ return [() => {}, false];
440
+ },
441
+ readContext: (unsupportedHook: any),
442
+ useContext: (unsupportedHook: any),
443
+ useReducer: (unsupportedHook: any),
444
+ useRef: (unsupportedHook: any),
445
+ useState: (unsupportedHook: any),
446
+ useLayoutEffect: (unsupportedHook: any),
447
+ useImperativeHandle: (unsupportedHook: any),
448
+ useEffect: (unsupportedHook: any),
449
+ useOpaqueIdentifier: (unsupportedHook: any),
450
+ useMutableSource: (unsupportedHook: any),
451
+};
packages/react-transport-dom-relay/src/__tests__/ReactFlightDOMRelay-test.internal.js
+89
@@ -124,4 +124,93 @@ describe('ReactFlightDOMRelay', () => {
124
125
expect(container.innerHTML).toEqual('<span>Hello, Seb Smith</span>');
126
});
127
+
128
+ // @gate experimental
129
+ it('can reasonably handle different element types', () => {
130
+ const {
131
+ forwardRef,
132
+ memo,
133
+ Fragment,
134
+ StrictMode,
135
+ Profiler,
136
+ Suspense,
137
+ SuspenseList,
138
+ } = React;
139
+
140
+ const Inner = memo(
141
+ forwardRef((props, ref) => {
142
+ return <div ref={ref}>{'Hello ' + props.name}</div>;
143
+ }),
144
+ );
145
+
146
+ function Foo() {
147
+ return {
148
+ bar: (
149
+ <div>
150
+ <Fragment>Fragment child</Fragment>
151
+ <Profiler>Profiler child</Profiler>
152
+ <StrictMode>StrictMode child</StrictMode>
153
+ <Suspense fallback="Loading...">Suspense child</Suspense>
154
+ <SuspenseList fallback="Loading...">
155
+ {'SuspenseList row 1'}
156
+ {'SuspenseList row 2'}
157
+ </SuspenseList>
158
+ <Inner name="world" />
159
+ </div>
160
+ ),
161
+ };
162
+ }
163
+ const transport = [];
164
+ ReactDOMFlightRelayServer.render(
165
+ {
166
+ foo: <Foo />,
167
+ },
168
+ transport,
169
+ );
170
+
171
+ const model = readThrough(transport);
172
+ expect(model).toEqual({
173
+ foo: {
174
+ bar: (
175
+ <div>
176
+ {'Fragment child'}
177
+ {'Profiler child'}
178
+ {'StrictMode child'}
179
+ {'Suspense child'}
180
+ {['SuspenseList row 1', 'SuspenseList row 2']}
181
+ <div>Hello world</div>
182
+ </div>
183
+ ),
184
+ },
185
+ });
186
+ });
187
+
188
+ it('can handle a subset of Hooks', () => {
189
+ const {useMemo, useCallback} = React;
190
+ function Inner({x}) {
191
+ const foo = useMemo(() => x + x, [x]);
192
+ const bar = useCallback(() => 10 + foo, [foo]);
193
+ return bar();
194
+ }
195
+
196
+ function Foo() {
197
+ return {
198
+ bar: <Inner x={2} />,
199
+ };
200
+ }
201
+ const transport = [];
202
+ ReactDOMFlightRelayServer.render(
203
+ {
204
+ foo: <Foo />,
205
+ },
206
+ transport,
207
+ );
208
+
209
+ const model = readThrough(transport);
210
+ expect(model).toEqual({
211
+ foo: {
212
+ bar: 14,
213
+ },
214
+ });
215
+ });
216
});
scripts/error-codes/codes.json
+2
-1
@@ -360,5 +360,6 @@
360
"369": "ReactDOM.createEventHandle: setter called on an invalid target. Provide a valid EventTarget or an element managed by React.",
361
"370": "ReactDOM.createEventHandle: setter called with an invalid callback. The callback must be a function.",
362
"371": "Text string must be rendered within a <Text> component.\n\nText: %s",
363
- "372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React."
363
+ "372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React.",
364
+ "373": "This Hook is not supported in Server Components."
365
}