[Fizz] Implement New Context (#21255)
* Add NewContext module This implements a reverse linked list tree containing the previous contexts. * Implement recursive algorithm This algorithm pops the contexts back to a shared ancestor on the way down the stack and then pushes new contexts in reverse order up the stack. * Move isPrimaryRenderer to ServerFormatConfig This is primarily intended to be used to support renderToString with a separate build than the main one. This allows them to be nested. * Wire up more element type matchers * Wire up Context Provider type * Wire up Context Consumer * Test * Implement reader in class * Update error codez
Sebastian Markbåge committed
Apr 14, 2021 at 14:45 UTC
4f76a28c93c56e3f0c03150460fd4236880e0c94
8 files changed
+567
-34
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+66
@@ -727,4 +727,70 @@ describe('ReactDOMFizzServer', () => {
727
</div>,
728
);
729
});
730
+
731
+ // @gate experimental
732
+ it('should resume the context from where it left off', async () => {
733
+ const ContextA = React.createContext('A0');
734
+ const ContextB = React.createContext('B0');
735
+
736
+ function PrintA() {
737
+ return (
738
+ <ContextA.Consumer>{value => <Text text={value} />}</ContextA.Consumer>
739
+ );
740
+ }
741
+
742
+ class PrintB extends React.Component {
743
+ static contextType = ContextB;
744
+ render() {
745
+ return <Text text={this.context} />;
746
+ }
747
+ }
748
+
749
+ function AsyncParent({text, children}) {
750
+ return (
751
+ <>
752
+ <AsyncText text={text} />
753
+ <b>{children}</b>
754
+ </>
755
+ );
756
+ }
757
+
758
+ await act(async () => {
759
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
760
+ <div>
761
+ <PrintA />
762
+ <div>
763
+ <ContextA.Provider value="A0.1">
764
+ <Suspense fallback={<Text text="Loading..." />}>
765
+ <AsyncParent text="Child:">
766
+ <PrintA />
767
+ </AsyncParent>
768
+ <PrintB />
769
+ </Suspense>
770
+ </ContextA.Provider>
771
+ </div>
772
+ <PrintA />
773
+ </div>,
774
+ writable,
775
+ );
776
+ startWriting();
777
+ });
778
+ expect(getVisibleChildren(container)).toEqual(
779
+ <div>
780
+ A0<div>Loading...</div>A0
781
+ </div>,
782
+ );
783
+ await act(async () => {
784
+ resolveText('Child:');
785
+ });
786
+ expect(getVisibleChildren(container)).toEqual(
787
+ <div>
788
+ A0
789
+ <div>
790
+ Child:<b>A0.1</b>B0
791
+ </div>
792
+ A0
793
+ </div>,
794
+ );
795
+ });
796
});
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+4
@@ -48,6 +48,10 @@ import hasOwnProperty from 'shared/hasOwnProperty';
48
import sanitizeURL from '../shared/sanitizeURL';
49
import isArray from 'shared/isArray';
50
51
+// Used to distinguish these contexts from ones used in other renderers.
52
+// E.g. this can be used to distinguish legacy renderers from this modern one.
53
+export const isPrimaryRenderer = true;
54
+
55
// Per response, global state that is not contextual to the rendering subtree.
56
export type ResponseState = {
57
placeholderPrefix: PrecomputedChunk,
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+2
@@ -23,6 +23,8 @@ import {
23
24
import invariant from 'shared/invariant';
25
26
+export const isPrimaryRenderer = true;
27
+
28
// Every list of children or string is null terminated.
29
const END_TAG = 0;
30
// Tree node tags.
packages/react-server/src/ReactFizzClassComponent.js
+3
-6
@@ -8,6 +8,7 @@
8
*/
9
10
import {emptyContextObject} from './ReactFizzContext';
11
+import {readContext} from './ReactFizzNewContext';
12
13
import {disableLegacyContext} from 'shared/ReactFeatureFlags';
14
import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap';
@@ -211,9 +212,7 @@ export function constructClassInstance(
212
}
213
214
if (typeof contextType === 'object' && contextType !== null) {
214
- // TODO: Implement Context.
215
- // context = readContext((contextType: any));
216
- throw new Error('Context is not yet implemented.');
215
+ context = readContext((contextType: any));
216
} else if (!disableLegacyContext) {
217
context = maskedLegacyContext;
218
}
@@ -617,9 +616,7 @@ export function mountClassInstance(
616
617
const contextType = ctor.contextType;
618
if (typeof contextType === 'object' && contextType !== null) {
620
- // TODO: Implement Context.
621
- // instance.context = readContext(contextType);
622
- throw new Error('Context is not yet implemented.');
619
+ instance.context = readContext(contextType);
620
} else if (disableLegacyContext) {
621
instance.context = emptyContextObject;
622
} else {
packages/react-server/src/ReactFizzNewContext.js
new
+278
@@ -0,0 +1,278 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {ReactContext} from 'shared/ReactTypes';
11
+
12
+import {isPrimaryRenderer} from './ReactServerFormatConfig';
13
+
14
+import invariant from 'shared/invariant';
15
+
16
+let rendererSigil;
17
+if (__DEV__) {
18
+ // Use this to detect multiple renderers using the same context
19
+ rendererSigil = {};
20
+}
21
+
22
+// Used to store the parent path of all context overrides in a shared linked list.
23
+// Forming a reverse tree.
24
+type ContextNode<T> = {
25
+ parent: null | ContextNode<any>,
26
+ depth: number, // Short hand to compute the depth of the tree at this node.
27
+ context: ReactContext<T>,
28
+ parentValue: T,
29
+ value: T,
30
+};
31
+
32
+// The structure of a context snapshot is an implementation of this file.
33
+// Currently, it's implemented as tracking the current active node.
34
+export opaque type ContextSnapshot = null | ContextNode<any>;
35
+
36
+export const rootContextSnapshot: ContextSnapshot = null;
37
+
38
+// We assume that this runtime owns the "current" field on all ReactContext instances.
39
+// This global (actually thread local) state represents what state all those "current",
40
+// fields are currently in.
41
+let currentActiveSnapshot: ContextSnapshot = null;
42
+
43
+function popNode(prev: ContextNode<any>): void {
44
+ if (isPrimaryRenderer) {
45
+ prev.context._currentValue = prev.parentValue;
46
+ } else {
47
+ prev.context._currentValue2 = prev.parentValue;
48
+ }
49
+}
50
+
51
+function pushNode(next: ContextNode<any>): void {
52
+ if (isPrimaryRenderer) {
53
+ next.context._currentValue = next.value;
54
+ } else {
55
+ next.context._currentValue2 = next.value;
56
+ }
57
+}
58
+
59
+function popToNearestCommonAncestor(
60
+ prev: ContextNode<any>,
61
+ next: ContextNode<any>,
62
+): void {
63
+ if (prev === next) {
64
+ // We've found a shared ancestor. We don't need to pop nor reapply this one or anything above.
65
+ } else {
66
+ popNode(prev);
67
+ const parentPrev = prev.parent;
68
+ const parentNext = next.parent;
69
+ if (parentPrev === null) {
70
+ invariant(
71
+ parentNext === null,
72
+ 'The stacks must reach the root at the same time. This is a bug in React.',
73
+ );
74
+ } else {
75
+ invariant(
76
+ parentNext !== null,
77
+ 'The stacks must reach the root at the same time. This is a bug in React.',
78
+ );
79
+ popToNearestCommonAncestor(parentPrev, parentNext);
80
+ // On the way back, we push the new ones that weren't common.
81
+ pushNode(next);
82
+ }
83
+ }
84
+}
85
+
86
+function popAllPrevious(prev: ContextNode<any>): void {
87
+ popNode(prev);
88
+ const parentPrev = prev.parent;
89
+ if (parentPrev !== null) {
90
+ popAllPrevious(parentPrev);
91
+ }
92
+}
93
+
94
+function pushAllNext(next: ContextNode<any>): void {
95
+ const parentNext = next.parent;
96
+ if (parentNext !== null) {
97
+ pushAllNext(parentNext);
98
+ }
99
+ pushNode(next);
100
+}
101
+
102
+function popPreviousToCommonLevel(
103
+ prev: ContextNode<any>,
104
+ next: ContextNode<any>,
105
+): void {
106
+ popNode(prev);
107
+ const parentPrev = prev.parent;
108
+ invariant(
109
+ parentPrev !== null,
110
+ 'The depth must equal at least at zero before reaching the root. This is a bug in React.',
111
+ );
112
+ if (parentPrev.depth === next.depth) {
113
+ // We found the same level. Now we just need to find a shared ancestor.
114
+ popToNearestCommonAncestor(parentPrev, next);
115
+ } else {
116
+ // We must still be deeper.
117
+ popPreviousToCommonLevel(parentPrev, next);
118
+ }
119
+}
120
+
121
+function popNextToCommonLevel(
122
+ prev: ContextNode<any>,
123
+ next: ContextNode<any>,
124
+): void {
125
+ const parentNext = next.parent;
126
+ invariant(
127
+ parentNext !== null,
128
+ 'The depth must equal at least at zero before reaching the root. This is a bug in React.',
129
+ );
130
+ if (prev.depth === parentNext.depth) {
131
+ // We found the same level. Now we just need to find a shared ancestor.
132
+ popToNearestCommonAncestor(prev, parentNext);
133
+ } else {
134
+ // We must still be deeper.
135
+ popNextToCommonLevel(prev, parentNext);
136
+ }
137
+ pushNode(next);
138
+}
139
+
140
+// Perform context switching to the new snapshot.
141
+// To make it cheap to read many contexts, while not suspending, we make the switch eagerly by
142
+// updating all the context's current values. That way reads, always just read the current value.
143
+// At the cost of updating contexts even if they're never read by this subtree.
144
+export function switchContext(newSnapshot: ContextSnapshot): void {
145
+ // The basic algorithm we need to do is to pop back any contexts that are no longer on the stack.
146
+ // We also need to update any new contexts that are now on the stack with the deepest value.
147
+ // The easiest way to update new contexts is to just reapply them in reverse order from the
148
+ // perspective of the backpointers. To avoid allocating a lot when switching, we use the stack
149
+ // for that. Therefore this algorithm is recursive.
150
+ // 1) First we pop which ever snapshot tree was deepest. Popping old contexts as we go.
151
+ // 2) Then we find the nearest common ancestor from there. Popping old contexts as we go.
152
+ // 3) Then we reapply new contexts on the way back up the stack.
153
+ const prev = currentActiveSnapshot;
154
+ const next = newSnapshot;
155
+ if (prev !== next) {
156
+ if (prev === null) {
157
+ // $FlowFixMe: This has to be non-null since it's not equal to prev.
158
+ pushAllNext(next);
159
+ } else if (next === null) {
160
+ popAllPrevious(prev);
161
+ } else if (prev.depth === next.depth) {
162
+ popToNearestCommonAncestor(prev, next);
163
+ } else if (prev.depth > next.depth) {
164
+ popPreviousToCommonLevel(prev, next);
165
+ } else {
166
+ popNextToCommonLevel(prev, next);
167
+ }
168
+ currentActiveSnapshot = next;
169
+ }
170
+}
171
+
172
+export function pushProvider<T>(
173
+ context: ReactContext<T>,
174
+ nextValue: T,
175
+): ContextSnapshot {
176
+ let prevValue;
177
+ if (isPrimaryRenderer) {
178
+ prevValue = context._currentValue;
179
+ context._currentValue = nextValue;
180
+ if (__DEV__) {
181
+ if (
182
+ context._currentRenderer !== undefined &&
183
+ context._currentRenderer !== null &&
184
+ context._currentRenderer !== rendererSigil
185
+ ) {
186
+ console.error(
187
+ 'Detected multiple renderers concurrently rendering the ' +
188
+ 'same context provider. This is currently unsupported.',
189
+ );
190
+ }
191
+ context._currentRenderer = rendererSigil;
192
+ }
193
+ } else {
194
+ prevValue = context._currentValue2;
195
+ context._currentValue2 = nextValue;
196
+ if (__DEV__) {
197
+ if (
198
+ context._currentRenderer2 !== undefined &&
199
+ context._currentRenderer2 !== null &&
200
+ context._currentRenderer2 !== rendererSigil
201
+ ) {
202
+ console.error(
203
+ 'Detected multiple renderers concurrently rendering the ' +
204
+ 'same context provider. This is currently unsupported.',
205
+ );
206
+ }
207
+ context._currentRenderer2 = rendererSigil;
208
+ }
209
+ }
210
+ const prevNode = currentActiveSnapshot;
211
+ const newNode: ContextNode<T> = {
212
+ parent: prevNode,
213
+ depth: prevNode === null ? 0 : prevNode.depth + 1,
214
+ context: context,
215
+ parentValue: prevValue,
216
+ value: nextValue,
217
+ };
218
+ currentActiveSnapshot = newNode;
219
+ return newNode;
220
+}
221
+
222
+export function popProvider<T>(context: ReactContext<T>): ContextSnapshot {
223
+ const prevSnapshot = currentActiveSnapshot;
224
+ invariant(
225
+ prevSnapshot !== null,
226
+ 'Tried to pop a Context at the root of the app. This is a bug in React.',
227
+ );
228
+ if (__DEV__) {
229
+ if (prevSnapshot.context !== context) {
230
+ console.error(
231
+ 'The parent context is not the expected context. This is probably a bug in React.',
232
+ );
233
+ }
234
+ }
235
+ if (isPrimaryRenderer) {
236
+ prevSnapshot.context._currentValue = prevSnapshot.parentValue;
237
+ if (__DEV__) {
238
+ if (
239
+ context._currentRenderer !== undefined &&
240
+ context._currentRenderer !== null &&
241
+ context._currentRenderer !== rendererSigil
242
+ ) {
243
+ console.error(
244
+ 'Detected multiple renderers concurrently rendering the ' +
245
+ 'same context provider. This is currently unsupported.',
246
+ );
247
+ }
248
+ context._currentRenderer = rendererSigil;
249
+ }
250
+ } else {
251
+ prevSnapshot.context._currentValue2 = prevSnapshot.parentValue;
252
+ if (__DEV__) {
253
+ if (
254
+ context._currentRenderer2 !== undefined &&
255
+ context._currentRenderer2 !== null &&
256
+ context._currentRenderer2 !== rendererSigil
257
+ ) {
258
+ console.error(
259
+ 'Detected multiple renderers concurrently rendering the ' +
260
+ 'same context provider. This is currently unsupported.',
261
+ );
262
+ }
263
+ context._currentRenderer2 = rendererSigil;
264
+ }
265
+ }
266
+ return (currentActiveSnapshot = prevSnapshot.parent);
267
+}
268
+
269
+export function getActiveContext(): ContextSnapshot {
270
+ return currentActiveSnapshot;
271
+}
272
+
273
+export function readContext<T>(context: ReactContext<T>): T {
274
+ const value = isPrimaryRenderer
275
+ ? context._currentValue
276
+ : context._currentValue2;
277
+ return value;
278
+}
packages/react-server/src/ReactFizzServer.js
+208
-27
@@ -13,12 +13,18 @@ import type {
13
Chunk,
14
PrecomputedChunk,
15
} from './ReactServerStreamConfig';
16
-import type {ReactNodeList} from 'shared/ReactTypes';
16
+import type {
17
+ ReactNodeList,
18
+ ReactContext,
19
+ ReactProviderType,
20
+} from 'shared/ReactTypes';
21
+import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
22
import type {
23
SuspenseBoundaryID,
24
ResponseState,
25
FormatContext,
26
} from './ReactServerFormatConfig';
27
+import type {ContextSnapshot} from './ReactFizzNewContext';
28
29
import {
30
scheduleWork,
@@ -56,6 +62,15 @@ import {
62
processChildContext,
63
emptyContextObject,
64
} from './ReactFizzContext';
65
+import {
66
+ readContext,
67
+ rootContextSnapshot,
68
+ switchContext,
69
+ getActiveContext,
70
+ pushProvider,
71
+ popProvider,
72
+} from './ReactFizzNewContext';
73
+
74
import {
75
getIteratorFn,
76
REACT_ELEMENT_TYPE,
@@ -68,6 +83,10 @@ import {
83
REACT_PROFILER_TYPE,
84
REACT_SUSPENSE_LIST_TYPE,
85
REACT_FRAGMENT_TYPE,
86
+ REACT_FORWARD_REF_TYPE,
87
+ REACT_MEMO_TYPE,
88
+ REACT_PROVIDER_TYPE,
89
+ REACT_CONTEXT_TYPE,
90
} from 'shared/ReactSymbols';
91
import ReactSharedInternals from 'shared/ReactSharedInternals';
92
import {
@@ -104,6 +123,7 @@ type Task = {
123
blockedSegment: Segment, // the segment we'll write to
124
abortSet: Set<Task>, // the abortable set that this task belongs to
125
legacyContext: LegacyContext, // the current legacy context that this task is executing in
126
+ context: ContextSnapshot, // the current new context that this task is executing in
127
assignID: null | SuspenseBoundaryID, // id to assign to the content
128
};
129
@@ -220,6 +240,7 @@ export function createRequest(
240
rootSegment,
241
abortSet,
242
emptyContextObject,
243
+ rootContextSnapshot,
244
null,
245
);
246
pingedTasks.push(rootTask);
@@ -257,6 +278,7 @@ function createTask(
278
blockedSegment: Segment,
279
abortSet: Set<Task>,
280
legacyContext: LegacyContext,
281
+ context: ContextSnapshot,
282
assignID: null | SuspenseBoundaryID,
283
): Task {
284
request.allPendingTasks++;
@@ -272,6 +294,7 @@ function createTask(
294
blockedSegment,
295
abortSet,
296
legacyContext,
297
+ context,
298
assignID,
299
};
300
abortSet.add(task);
@@ -394,6 +417,7 @@ function renderSuspenseBoundary(
417
boundarySegment,
418
fallbackAbortSet,
419
task.legacyContext,
420
+ task.context,
421
newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
422
);
423
// TODO: This should be queued at a separate lower priority queue so that we only work
@@ -529,6 +553,7 @@ let didWarnAboutReassigningProps = false;
553
const didWarnAboutDefaultPropsOnFunctionComponent = {};
554
let didWarnAboutGenerators = false;
555
let didWarnAboutMaps = false;
556
+let hasWarnedAboutUsingContextAsConsumer = false;
557
558
// This would typically be a function component but we still support module pattern
559
// components for some reason.
@@ -694,49 +719,185 @@ function validateFunctionComponentInDev(Component: any): void {
719
}
720
}
721
722
+function renderForwardRef(
723
+ request: Request,
724
+ task: Task,
725
+ type: any,
726
+ props: Object,
727
+): void {
728
+ throw new Error('Not yet implemented element type.');
729
+}
730
+
731
+function renderMemo(
732
+ request: Request,
733
+ task: Task,
734
+ type: any,
735
+ props: Object,
736
+): void {
737
+ throw new Error('Not yet implemented element type.');
738
+}
739
+
740
+function renderContextConsumer(
741
+ request: Request,
742
+ task: Task,
743
+ context: ReactContext<any>,
744
+ props: Object,
745
+): void {
746
+ // The logic below for Context differs depending on PROD or DEV mode. In
747
+ // DEV mode, we create a separate object for Context.Consumer that acts
748
+ // like a proxy to Context. This proxy object adds unnecessary code in PROD
749
+ // so we use the old behaviour (Context.Consumer references Context) to
750
+ // reduce size and overhead. The separate object references context via
751
+ // a property called "_context", which also gives us the ability to check
752
+ // in DEV mode if this property exists or not and warn if it does not.
753
+ if (__DEV__) {
754
+ if ((context: any)._context === undefined) {
755
+ // This may be because it's a Context (rather than a Consumer).
756
+ // Or it may be because it's older React where they're the same thing.
757
+ // We only want to warn if we're sure it's a new React.
758
+ if (context !== context.Consumer) {
759
+ if (!hasWarnedAboutUsingContextAsConsumer) {
760
+ hasWarnedAboutUsingContextAsConsumer = true;
761
+ console.error(
762
+ 'Rendering <Context> directly is not supported and will be removed in ' +
763
+ 'a future major release. Did you mean to render <Context.Consumer> instead?',
764
+ );
765
+ }
766
+ }
767
+ } else {
768
+ context = (context: any)._context;
769
+ }
770
+ }
771
+ const render = props.children;
772
+
773
+ if (__DEV__) {
774
+ if (typeof render !== 'function') {
775
+ console.error(
776
+ 'A context consumer was rendered with multiple children, or a child ' +
777
+ "that isn't a function. A context consumer expects a single child " +
778
+ 'that is a function. If you did pass a function, make sure there ' +
779
+ 'is no trailing or leading whitespace around it.',
780
+ );
781
+ }
782
+ }
783
+
784
+ const newValue = readContext(context);
785
+ const newChildren = render(newValue);
786
+
787
+ renderNodeDestructive(request, task, newChildren);
788
+}
789
+
790
+function renderContextProvider(
791
+ request: Request,
792
+ task: Task,
793
+ type: ReactProviderType<any>,
794
+ props: Object,
795
+): void {
796
+ const context = type._context;
797
+ const value = props.value;
798
+ const children = props.children;
799
+ let prevSnapshot;
800
+ if (__DEV__) {
801
+ prevSnapshot = task.context;
802
+ }
803
+ task.context = pushProvider(context, value);
804
+ renderNodeDestructive(request, task, children);
805
+ task.context = popProvider(context);
806
+ if (__DEV__) {
807
+ if (prevSnapshot !== task.context) {
808
+ console.error(
809
+ 'Popping the context provider did not return back to the original snapshot. This is a bug in React.',
810
+ );
811
+ }
812
+ }
813
+}
814
+
815
+function renderLazyComponent(
816
+ request: Request,
817
+ task: Task,
818
+ type: LazyComponentType<any, any>,
819
+ props: Object,
820
+): void {
821
+ throw new Error('Not yet implemented element type.');
822
+}
823
+
824
function renderElement(
825
request: Request,
826
task: Task,
827
type: any,
828
props: Object,
702
- node: ReactNodeList,
829
): void {
830
if (typeof type === 'function') {
831
if (shouldConstruct(type)) {
832
renderClassComponent(request, task, type, props);
833
+ return;
834
} else {
835
renderIndeterminateComponent(request, task, type, props);
836
+ return;
837
}
710
- } else if (typeof type === 'string') {
838
+ }
839
+ if (typeof type === 'string') {
840
renderHostElement(request, task, type, props);
712
- } else {
713
- switch (type) {
714
- // TODO: LegacyHidden acts the same as a fragment. This only works
715
- // because we currently assume that every instance of LegacyHidden is
716
- // accompanied by a host component wrapper. In the hidden mode, the host
717
- // component is given a `hidden` attribute, which ensures that the
718
- // initial HTML is not visible. To support the use of LegacyHidden as a
719
- // true fragment, without an extra DOM node, we would have to hide the
720
- // initial HTML in some other way.
721
- // TODO: Add REACT_OFFSCREEN_TYPE here too with the same capability.
722
- case REACT_LEGACY_HIDDEN_TYPE:
723
- case REACT_DEBUG_TRACING_MODE_TYPE:
724
- case REACT_STRICT_MODE_TYPE:
725
- case REACT_PROFILER_TYPE:
726
- case REACT_SUSPENSE_LIST_TYPE: // TODO: SuspenseList should control the boundaries.
727
- case REACT_FRAGMENT_TYPE: {
728
- renderNodeDestructive(request, task, props.children);
729
- break;
841
+ return;
842
+ }
843
+
844
+ switch (type) {
845
+ // TODO: LegacyHidden acts the same as a fragment. This only works
846
+ // because we currently assume that every instance of LegacyHidden is
847
+ // accompanied by a host component wrapper. In the hidden mode, the host
848
+ // component is given a `hidden` attribute, which ensures that the
849
+ // initial HTML is not visible. To support the use of LegacyHidden as a
850
+ // true fragment, without an extra DOM node, we would have to hide the
851
+ // initial HTML in some other way.
852
+ // TODO: Add REACT_OFFSCREEN_TYPE here too with the same capability.
853
+ case REACT_LEGACY_HIDDEN_TYPE:
854
+ case REACT_DEBUG_TRACING_MODE_TYPE:
855
+ case REACT_STRICT_MODE_TYPE:
856
+ case REACT_PROFILER_TYPE:
857
+ case REACT_SUSPENSE_LIST_TYPE: // TODO: SuspenseList should control the boundaries.
858
+ case REACT_FRAGMENT_TYPE: {
859
+ renderNodeDestructive(request, task, props.children);
860
+ return;
861
+ }
862
+ case REACT_SUSPENSE_TYPE: {
863
+ renderSuspenseBoundary(request, task, props);
864
+ return;
865
+ }
866
+ }
867
+
868
+ if (typeof type === 'object' && type !== null) {
869
+ switch (type.$$typeof) {
870
+ case REACT_FORWARD_REF_TYPE: {
871
+ renderForwardRef(request, task, type, props);
872
+ return;
873
}
731
- case REACT_SUSPENSE_TYPE: {
732
- renderSuspenseBoundary(request, task, props);
733
- break;
874
+ case REACT_MEMO_TYPE: {
875
+ renderMemo(request, task, type, props);
876
+ return;
877
}
735
- default: {
736
- throw new Error('Not yet implemented element type.');
878
+ case REACT_PROVIDER_TYPE: {
879
+ renderContextProvider(request, task, type, props);
880
+ return;
881
+ }
882
+ case REACT_CONTEXT_TYPE: {
883
+ renderContextConsumer(request, task, type, props);
884
+ return;
885
+ }
886
+ case REACT_LAZY_TYPE: {
887
+ renderLazyComponent(request, task, type, props);
888
+ return;
889
}
890
}
891
}
892
+
893
+ invariant(
894
+ false,
895
+ 'Element type is invalid: expected a string (for built-in ' +
896
+ 'components) or a class/function (for composite components) ' +
897
+ 'but got: %s.%s',
898
+ type == null ? type : typeof type,
899
+ '',
900
+ );
901
}
902
903
function validateIterable(iterable, iteratorFn: Function): void {
@@ -791,7 +952,7 @@ function renderNodeDestructive(
952
const element: React$Element<any> = (node: any);
953
const type = element.type;
954
const props = element.props;
794
- renderElement(request, task, type, props, node);
955
+ renderElement(request, task, type, props);
956
return;
957
}
958
case REACT_PORTAL_TYPE:
@@ -918,6 +1079,7 @@ function spawnNewSuspendedTask(
1079
newSegment,
1080
task.abortSet,
1081
task.legacyContext,
1082
+ task.context,
1083
task.assignID,
1084
);
1085
// We've delegated the assignment.
@@ -936,6 +1098,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
1098
// process.
1099
const previousFormatContext = task.blockedSegment.formatContext;
1100
const previousLegacyContext = task.legacyContext;
1101
+ const previousContext = task.context;
1102
try {
1103
return renderNodeDestructive(request, task, node);
1104
} catch (x) {
@@ -945,6 +1108,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
1108
// functions in case nothing throws so we don't use "finally" here.
1109
task.blockedSegment.formatContext = previousFormatContext;
1110
task.legacyContext = previousLegacyContext;
1111
+ task.context = previousContext;
1112
+ // Restore all active ReactContexts to what they were before.
1113
+ switchContext(previousContext);
1114
} else {
1115
// We assume that we don't need the correct context.
1116
// Let's terminate the rest of the tree and don't render any siblings.
@@ -1104,6 +1270,10 @@ function retryTask(request: Request, task: Task): void {
1270
// We completed this by other means before we had a chance to retry it.
1271
return;
1272
}
1273
+ // We restore the context to what it was when we suspended.
1274
+ // We don't restore it after we leave because it's likely that we'll end up
1275
+ // needing a very similar context soon again.
1276
+ switchContext(task.context);
1277
try {
1278
// We call the destructive form that mutates this task. That way if something
1279
// suspends again, we can reuse the same task instead of spawning a new one.
@@ -1129,6 +1299,7 @@ function performWork(request: Request): void {
1299
if (request.status === CLOSED) {
1300
return;
1301
}
1302
+ const prevContext = getActiveContext();
1303
const prevDispatcher = ReactCurrentDispatcher.current;
1304
ReactCurrentDispatcher.current = Dispatcher;
1305
@@ -1148,6 +1319,16 @@ function performWork(request: Request): void {
1319
fatalError(request, error);
1320
} finally {
1321
ReactCurrentDispatcher.current = prevDispatcher;
1322
+ if (prevDispatcher === Dispatcher) {
1323
+ // This means that we were in a reentrant work loop. This could happen
1324
+ // in a renderer that supports synchronous work like renderToString,
1325
+ // when it's called from within another renderer.
1326
+ // Normally we don't bother switching the contexts to their root/default
1327
+ // values when leaving because we'll likely need the same or similar
1328
+ // context again. However, when we're inside a synchronous loop like this
1329
+ // we'll to restore the context to what it was before returning.
1330
+ switchContext(prevContext);
1331
+ }
1332
}
1333
}
1334
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+2
@@ -29,6 +29,8 @@ export opaque type ResponseState = mixed;
29
export opaque type FormatContext = mixed;
30
export opaque type SuspenseBoundaryID = mixed;
31
32
+export const isPrimaryRenderer = false;
33
+
34
export const getChildFormatContext = $$$hostConfig.getChildFormatContext;
35
export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
36
export const pushEmpty = $$$hostConfig.pushEmpty;
scripts/error-codes/codes.json
+4
-1
@@ -388,5 +388,8 @@
388
"397": "Unknown insertion mode. This is a bug in React.",
389
"398": "`dangerouslySetInnerHTML` does not work on <option>.",
390
"399": "%s is a self-closing tag and must neither have `children` nor use `dangerouslySetInnerHTML`.",
391
- "400": "menuitems cannot have `children` nor `dangerouslySetInnerHTML`."
391
+ "400": "menuitems cannot have `children` nor `dangerouslySetInnerHTML`.",
392
+ "401": "The stacks must reach the root at the same time. This is a bug in React.",
393
+ "402": "The depth must equal at least at zero before reaching the root. This is a bug in React.",
394
+ "403": "Tried to pop a Context at the root of the app. This is a bug in React."
395
}