Skip to content

Commit c1ac052

Browse files
authored
[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
1 parent 1eaafc9 commit c1ac052

File tree

3 files changed

+166
-5
lines changed

3 files changed

+166
-5
lines changed

packages/react-server/src/ReactFlightServer.js

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
1011
import type {
1112
Destination,
1213
Chunk,
@@ -29,12 +30,24 @@ import {
2930

3031
import {
3132
REACT_BLOCK_TYPE,
32-
REACT_SERVER_BLOCK_TYPE,
3333
REACT_ELEMENT_TYPE,
34+
REACT_DEBUG_TRACING_MODE_TYPE,
35+
REACT_FORWARD_REF_TYPE,
3436
REACT_FRAGMENT_TYPE,
3537
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,
3647
} from 'shared/ReactSymbols';
3748

49+
import * as React from 'react';
50+
import ReactSharedInternals from 'shared/ReactSharedInternals';
3851
import invariant from 'shared/invariant';
3952

4053
type ReactJSONValue =
@@ -74,6 +87,8 @@ export type Request = {
7487
toJSON: (key: string, value: ReactModel) => ReactJSONValue,
7588
};
7689

90+
const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
91+
7792
export function createRequest(
7893
model: ReactModel,
7994
destination: Destination,
@@ -110,11 +125,33 @@ function attemptResolveElement(element: React$Element<any>): ReactModel {
110125
return [REACT_ELEMENT_TYPE, type, element.key, element.props];
111126
} else if (type[0] === REACT_SERVER_BLOCK_TYPE) {
112127
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+
) {
114141
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+
}
117153
}
154+
invariant(false, 'Unsupported type.');
118155
}
119156

120157
function pingSegment(request: Request, segment: Segment): void {
@@ -236,9 +273,11 @@ export function resolveModelToJSON(
236273
value !== null &&
237274
value.$$typeof === REACT_ELEMENT_TYPE
238275
) {
276+
const prevDispatcher = ReactCurrentDispatcher.current;
239277
// TODO: Concatenate keys of parents onto children.
240278
const element: React$Element<any> = (value: any);
241279
try {
280+
ReactCurrentDispatcher.current = Dispatcher;
242281
// Attempt to render the server component.
243282
value = attemptResolveElement(element);
244283
} catch (x) {
@@ -253,6 +292,8 @@ export function resolveModelToJSON(
253292
// Something errored. Don't bother encoding anything up to here.
254293
throw x;
255294
}
295+
} finally {
296+
ReactCurrentDispatcher.current = prevDispatcher;
256297
}
257298
}
258299

@@ -378,3 +419,33 @@ export function startFlowing(request: Request): void {
378419
request.flowing = true;
379420
flushCompletedChunks(request);
380421
}
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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,93 @@ describe('ReactFlightDOMRelay', () => {
124124

125125
expect(container.innerHTML).toEqual('<span>Hello, Seb Smith</span>');
126126
});
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+
});
127216
});

scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,5 +360,6 @@
360360
"369": "ReactDOM.createEventHandle: setter called on an invalid target. Provide a valid EventTarget or an element managed by React.",
361361
"370": "ReactDOM.createEventHandle: setter called with an invalid callback. The callback must be a function.",
362362
"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."
364365
}

0 commit comments

Comments
 (0)