Skip to content

Commit 46f912f

Browse files
authored
[react-core] Add more support for experimental React Scope API (#16621)
1 parent 2c1e6bf commit 46f912f

File tree

7 files changed

+229
-14
lines changed

7 files changed

+229
-14
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
enableSuspenseServerRenderer,
3333
enableFundamentalAPI,
3434
enableFlareAPI,
35+
enableScopeAPI,
3536
} from 'shared/ReactFeatureFlags';
3637

3738
import {
@@ -48,6 +49,7 @@ import {
4849
REACT_LAZY_TYPE,
4950
REACT_MEMO_TYPE,
5051
REACT_FUNDAMENTAL_TYPE,
52+
REACT_SCOPE_TYPE,
5153
} from 'shared/ReactSymbols';
5254

5355
import {
@@ -1285,6 +1287,31 @@ class ReactDOMServerRenderer {
12851287
);
12861288
}
12871289
}
1290+
// eslint-disable-next-line-no-fallthrough
1291+
case REACT_SCOPE_TYPE: {
1292+
if (enableScopeAPI) {
1293+
const nextChildren = toArray(
1294+
((nextChild: any): ReactElement).props.children,
1295+
);
1296+
const frame: Frame = {
1297+
type: null,
1298+
domNamespace: parentNamespace,
1299+
children: nextChildren,
1300+
childIndex: 0,
1301+
context: context,
1302+
footer: '',
1303+
};
1304+
if (__DEV__) {
1305+
((frame: any): FrameDev).debugElementStack = [];
1306+
}
1307+
this.stack.push(frame);
1308+
return '';
1309+
}
1310+
invariant(
1311+
false,
1312+
'ReactDOMServer does not yet support scope components.',
1313+
);
1314+
}
12881315
}
12891316
}
12901317

packages/react-reconciler/src/ReactFiberScope.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import type {
1414
ReactScopeMethods,
1515
} from 'shared/ReactTypes';
1616

17+
import {getPublicInstance} from './ReactFiberHostConfig';
18+
1719
import {
1820
HostComponent,
1921
SuspenseComponent,
2022
ScopeComponent,
2123
} from 'shared/ReactWorkTags';
24+
import {enableScopeAPI} from 'shared/ReactFeatureFlags';
2225

2326
function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
2427
return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
@@ -33,19 +36,21 @@ function collectScopedNodes(
3336
fn: (type: string | Object, props: Object) => boolean,
3437
scopedNodes: Array<any>,
3538
): void {
36-
if (node.tag === HostComponent) {
37-
const {type, memoizedProps} = node;
38-
if (fn(type, memoizedProps) === true) {
39-
scopedNodes.push(node.stateNode);
39+
if (enableScopeAPI) {
40+
if (node.tag === HostComponent) {
41+
const {type, memoizedProps} = node;
42+
if (fn(type, memoizedProps) === true) {
43+
scopedNodes.push(getPublicInstance(node.stateNode));
44+
}
4045
}
41-
}
42-
let child = node.child;
46+
let child = node.child;
4347

44-
if (isFiberSuspenseAndTimedOut(node)) {
45-
child = getSuspenseFallbackChild(node);
46-
}
47-
if (child !== null) {
48-
collectScopedNodesFromChildren(child, fn, scopedNodes);
48+
if (isFiberSuspenseAndTimedOut(node)) {
49+
child = getSuspenseFallbackChild(node);
50+
}
51+
if (child !== null) {
52+
collectScopedNodesFromChildren(child, fn, scopedNodes);
53+
}
4954
}
5055
}
5156

packages/react-reconciler/src/__tests__/ReactScope-test.internal.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,5 +164,185 @@ describe('ReactScope', () => {
164164
const aChildren = refA.current.getChildren();
165165
expect(aChildren).toEqual([refC.current]);
166166
});
167+
168+
it('scopes support server-side rendering and hydration', () => {
169+
const TestScope = React.unstable_createScope((type, props) => true);
170+
const ReactDOMServer = require('react-dom/server');
171+
const scopeRef = React.createRef();
172+
const divRef = React.createRef();
173+
const spanRef = React.createRef();
174+
const aRef = React.createRef();
175+
176+
function Test({toggle}) {
177+
return (
178+
<div>
179+
<TestScope ref={scopeRef}>
180+
<div ref={divRef}>DIV</div>
181+
<span ref={spanRef}>SPAN</span>
182+
<a ref={aRef}>A</a>
183+
</TestScope>
184+
<div>Outside content!</div>
185+
</div>
186+
);
187+
}
188+
const html = ReactDOMServer.renderToString(<Test />);
189+
expect(html).toBe(
190+
'<div data-reactroot=""><div>DIV</div><span>SPAN</span><a>A</a><div>Outside content!</div></div>',
191+
);
192+
container.innerHTML = html;
193+
ReactDOM.hydrate(<Test />, container);
194+
const nodes = scopeRef.current.getScopedNodes();
195+
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
196+
});
197+
});
198+
199+
describe('ReactTestRenderer', () => {
200+
let ReactTestRenderer;
201+
202+
beforeEach(() => {
203+
ReactTestRenderer = require('react-test-renderer');
204+
});
205+
206+
it('getScopedNodes() works as intended', () => {
207+
const TestScope = React.unstable_createScope((type, props) => true);
208+
const scopeRef = React.createRef();
209+
const divRef = React.createRef();
210+
const spanRef = React.createRef();
211+
const aRef = React.createRef();
212+
213+
function Test({toggle}) {
214+
return toggle ? (
215+
<TestScope ref={scopeRef}>
216+
<div ref={divRef}>DIV</div>
217+
<span ref={spanRef}>SPAN</span>
218+
<a ref={aRef}>A</a>
219+
</TestScope>
220+
) : (
221+
<TestScope ref={scopeRef}>
222+
<a ref={aRef}>A</a>
223+
<div ref={divRef}>DIV</div>
224+
<span ref={spanRef}>SPAN</span>
225+
</TestScope>
226+
);
227+
}
228+
229+
const renderer = ReactTestRenderer.create(<Test toggle={true} />, {
230+
createNodeMock: element => {
231+
return element;
232+
},
233+
});
234+
let nodes = scopeRef.current.getScopedNodes();
235+
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
236+
renderer.update(<Test toggle={false} />);
237+
nodes = scopeRef.current.getScopedNodes();
238+
expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]);
239+
});
240+
241+
it('mixed getParent() and getScopedNodes() works as intended', () => {
242+
const TestScope = React.unstable_createScope((type, props) => true);
243+
const TestScope2 = React.unstable_createScope((type, props) => true);
244+
const refA = React.createRef();
245+
const refB = React.createRef();
246+
const refC = React.createRef();
247+
const refD = React.createRef();
248+
const spanA = React.createRef();
249+
const spanB = React.createRef();
250+
const divA = React.createRef();
251+
const divB = React.createRef();
252+
253+
function Test() {
254+
return (
255+
<div>
256+
<TestScope ref={refA}>
257+
<span ref={spanA}>
258+
<TestScope2 ref={refB}>
259+
<div ref={divA}>
260+
<TestScope ref={refC}>
261+
<span ref={spanB}>
262+
<TestScope2 ref={refD}>
263+
<div ref={divB}>>Hello world</div>
264+
</TestScope2>
265+
</span>
266+
</TestScope>
267+
</div>
268+
</TestScope2>
269+
</span>
270+
</TestScope>
271+
</div>
272+
);
273+
}
274+
275+
ReactTestRenderer.create(<Test />, {
276+
createNodeMock: element => {
277+
return element;
278+
},
279+
});
280+
const dParent = refD.current.getParent();
281+
expect(dParent).not.toBe(null);
282+
expect(dParent.getScopedNodes()).toEqual([
283+
divA.current,
284+
spanB.current,
285+
divB.current,
286+
]);
287+
const cParent = refC.current.getParent();
288+
expect(cParent).not.toBe(null);
289+
expect(cParent.getScopedNodes()).toEqual([
290+
spanA.current,
291+
divA.current,
292+
spanB.current,
293+
divB.current,
294+
]);
295+
expect(refB.current.getParent()).toBe(null);
296+
expect(refA.current.getParent()).toBe(null);
297+
});
298+
299+
it('getChildren() works as intended', () => {
300+
const TestScope = React.unstable_createScope((type, props) => true);
301+
const TestScope2 = React.unstable_createScope((type, props) => true);
302+
const refA = React.createRef();
303+
const refB = React.createRef();
304+
const refC = React.createRef();
305+
const refD = React.createRef();
306+
const spanA = React.createRef();
307+
const spanB = React.createRef();
308+
const divA = React.createRef();
309+
const divB = React.createRef();
310+
311+
function Test() {
312+
return (
313+
<div>
314+
<TestScope ref={refA}>
315+
<span ref={spanA}>
316+
<TestScope2 ref={refB}>
317+
<div ref={divA}>
318+
<TestScope ref={refC}>
319+
<span ref={spanB}>
320+
<TestScope2 ref={refD}>
321+
<div ref={divB}>>Hello world</div>
322+
</TestScope2>
323+
</span>
324+
</TestScope>
325+
</div>
326+
</TestScope2>
327+
</span>
328+
</TestScope>
329+
</div>
330+
);
331+
}
332+
333+
ReactTestRenderer.create(<Test />, {
334+
createNodeMock: element => {
335+
return element;
336+
},
337+
});
338+
const dChildren = refD.current.getChildren();
339+
expect(dChildren).toBe(null);
340+
const cChildren = refC.current.getChildren();
341+
expect(cChildren).toBe(null);
342+
const bChildren = refB.current.getChildren();
343+
expect(bChildren).toEqual([refD.current]);
344+
const aChildren = refA.current.getChildren();
345+
expect(aChildren).toEqual([refC.current]);
346+
});
167347
});
168348
});

packages/react-test-renderer/src/ReactTestRenderer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
MemoComponent,
3838
SimpleMemoComponent,
3939
IncompleteClassComponent,
40+
ScopeComponent,
4041
} from 'shared/ReactWorkTags';
4142
import invariant from 'shared/invariant';
4243
import ReactVersion from 'shared/ReactVersion';
@@ -203,6 +204,7 @@ function toTree(node: ?Fiber) {
203204
case ForwardRef:
204205
case MemoComponent:
205206
case IncompleteClassComponent:
207+
case ScopeComponent:
206208
return childrenToTree(node.child);
207209
default:
208210
invariant(

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const warnAboutDeprecatedSetNativeProps = false;
2626
export const disableJavaScriptURLs = false;
2727
export const enableFlareAPI = true;
2828
export const enableFundamentalAPI = false;
29-
export const enableScopeAPI = false;
29+
export const enableScopeAPI = true;
3030
export const enableJSXTransformAPI = true;
3131
export const warnAboutUnmockedScheduler = true;
3232
export const flushSuspenseFallbacksInTests = true;

packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const enableFlareAPI = true;
7373

7474
export const enableFundamentalAPI = false;
7575

76-
export const enableScopeAPI = false;
76+
export const enableScopeAPI = true;
7777

7878
export const enableJSXTransformAPI = true;
7979

scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,5 +340,6 @@
340340
"339": "An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder().",
341341
"340": "Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.",
342342
"341": "We just came from a parent so we must have had a parent. This is a bug in React.",
343-
"342": "A React component suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display."
343+
"342": "A React component suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.",
344+
"343": "ReactDOMServer does not yet support scope components."
344345
}

0 commit comments

Comments
 (0)