[react-core] Add more support for experimental React Scope API (#16621)
Dominic Gannaway committed
Aug 30, 2019 at 18:27 UTC
46f912fd57604bd3c3949b5e8d6cc30a843ab1b9
7 files changed
+229
-14
packages/react-dom/src/server/ReactPartialRenderer.js
+27
@@ -32,6 +32,7 @@ import {
32
enableSuspenseServerRenderer,
33
enableFundamentalAPI,
34
enableFlareAPI,
35
+ enableScopeAPI,
36
} from 'shared/ReactFeatureFlags';
37
38
import {
@@ -48,6 +49,7 @@ import {
49
REACT_LAZY_TYPE,
50
REACT_MEMO_TYPE,
51
REACT_FUNDAMENTAL_TYPE,
52
+ REACT_SCOPE_TYPE,
53
} from 'shared/ReactSymbols';
54
55
import {
@@ -1285,6 +1287,31 @@ class ReactDOMServerRenderer {
1287
);
1288
}
1289
}
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
+ }
1315
}
1316
}
1317
packages/react-reconciler/src/ReactFiberScope.js
+16
-11
@@ -14,11 +14,14 @@ import type {
14
ReactScopeMethods,
15
} from 'shared/ReactTypes';
16
17
+import {getPublicInstance} from './ReactFiberHostConfig';
18
+
19
import {
20
HostComponent,
21
SuspenseComponent,
22
ScopeComponent,
23
} from 'shared/ReactWorkTags';
24
+import {enableScopeAPI} from 'shared/ReactFeatureFlags';
25
26
function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
27
return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
@@ -33,19 +36,21 @@ function collectScopedNodes(
36
fn: (type: string | Object, props: Object) => boolean,
37
scopedNodes: Array<any>,
38
): 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
+ }
45
}
41
- }
42
- let child = node.child;
46
+ let child = node.child;
47
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
+ }
54
}
55
}
56
packages/react-reconciler/src/__tests__/ReactScope-test.internal.js
+180
@@ -164,5 +164,185 @@ describe('ReactScope', () => {
164
const aChildren = refA.current.getChildren();
165
expect(aChildren).toEqual([refC.current]);
166
});
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
+ });
347
});
348
});
packages/react-test-renderer/src/ReactTestRenderer.js
+2
@@ -37,6 +37,7 @@ import {
37
MemoComponent,
38
SimpleMemoComponent,
39
IncompleteClassComponent,
40
+ ScopeComponent,
41
} from 'shared/ReactWorkTags';
42
import invariant from 'shared/invariant';
43
import ReactVersion from 'shared/ReactVersion';
@@ -203,6 +204,7 @@ function toTree(node: ?Fiber) {
204
case ForwardRef:
205
case MemoComponent:
206
case IncompleteClassComponent:
207
+ case ScopeComponent:
208
return childrenToTree(node.child);
209
default:
210
invariant(
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
-1
@@ -26,7 +26,7 @@ export const warnAboutDeprecatedSetNativeProps = false;
26
export const disableJavaScriptURLs = false;
27
export const enableFlareAPI = true;
28
export const enableFundamentalAPI = false;
29
-export const enableScopeAPI = false;
29
+export const enableScopeAPI = true;
30
export const enableJSXTransformAPI = true;
31
export const warnAboutUnmockedScheduler = true;
32
export const flushSuspenseFallbacksInTests = true;
packages/shared/forks/ReactFeatureFlags.www.js
+1
-1
@@ -73,7 +73,7 @@ export const enableFlareAPI = true;
73
74
export const enableFundamentalAPI = false;
75
76
-export const enableScopeAPI = false;
76
+export const enableScopeAPI = true;
77
78
export const enableJSXTransformAPI = true;
79
scripts/error-codes/codes.json
+2
-1
@@ -340,5 +340,6 @@
340
"339": "An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder().",
341
"340": "Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.",
342
"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."
345
}