Skip to content

Commit 4f76a28

Browse files
authored
[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
1 parent 6b3d86a commit 4f76a28

File tree

8 files changed

+567
-34
lines changed

8 files changed

+567
-34
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

+66
Original file line numberDiff line numberDiff line change
@@ -727,4 +727,70 @@ describe('ReactDOMFizzServer', () => {
727727
</div>,
728728
);
729729
});
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+
});
730796
});

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

+4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ import hasOwnProperty from 'shared/hasOwnProperty';
4848
import sanitizeURL from '../shared/sanitizeURL';
4949
import isArray from 'shared/isArray';
5050

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+
5155
// Per response, global state that is not contextual to the rendering subtree.
5256
export type ResponseState = {
5357
placeholderPrefix: PrecomputedChunk,

packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323

2424
import invariant from 'shared/invariant';
2525

26+
export const isPrimaryRenderer = true;
27+
2628
// Every list of children or string is null terminated.
2729
const END_TAG = 0;
2830
// Tree node tags.

packages/react-server/src/ReactFizzClassComponent.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import {emptyContextObject} from './ReactFizzContext';
11+
import {readContext} from './ReactFizzNewContext';
1112

1213
import {disableLegacyContext} from 'shared/ReactFeatureFlags';
1314
import {get as getInstance, set as setInstance} from 'shared/ReactInstanceMap';
@@ -211,9 +212,7 @@ export function constructClassInstance(
211212
}
212213

213214
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));
217216
} else if (!disableLegacyContext) {
218217
context = maskedLegacyContext;
219218
}
@@ -617,9 +616,7 @@ export function mountClassInstance(
617616

618617
const contextType = ctor.contextType;
619618
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);
623620
} else if (disableLegacyContext) {
624621
instance.context = emptyContextObject;
625622
} else {
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)