From 46f912fd57604bd3c3949b5e8d6cc30a843ab1b9 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 30 Aug 2019 18:27:14 +0100 Subject: [PATCH] [react-core] Add more support for experimental React Scope API (#16621) --- .../src/server/ReactPartialRenderer.js | 27 +++ .../react-reconciler/src/ReactFiberScope.js | 27 +-- .../src/__tests__/ReactScope-test.internal.js | 180 ++++++++++++++++++ .../src/ReactTestRenderer.js | 2 + .../ReactFeatureFlags.test-renderer.www.js | 2 +- .../shared/forks/ReactFeatureFlags.www.js | 2 +- scripts/error-codes/codes.json | 3 +- 7 files changed, 229 insertions(+), 14 deletions(-) diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index e29494349610..3b9655134c1b 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -32,6 +32,7 @@ import { enableSuspenseServerRenderer, enableFundamentalAPI, enableFlareAPI, + enableScopeAPI, } from 'shared/ReactFeatureFlags'; import { @@ -48,6 +49,7 @@ import { REACT_LAZY_TYPE, REACT_MEMO_TYPE, REACT_FUNDAMENTAL_TYPE, + REACT_SCOPE_TYPE, } from 'shared/ReactSymbols'; import { @@ -1285,6 +1287,31 @@ class ReactDOMServerRenderer { ); } } + // eslint-disable-next-line-no-fallthrough + case REACT_SCOPE_TYPE: { + if (enableScopeAPI) { + const nextChildren = toArray( + ((nextChild: any): ReactElement).props.children, + ); + const frame: Frame = { + type: null, + domNamespace: parentNamespace, + children: nextChildren, + childIndex: 0, + context: context, + footer: '', + }; + if (__DEV__) { + ((frame: any): FrameDev).debugElementStack = []; + } + this.stack.push(frame); + return ''; + } + invariant( + false, + 'ReactDOMServer does not yet support scope components.', + ); + } } } diff --git a/packages/react-reconciler/src/ReactFiberScope.js b/packages/react-reconciler/src/ReactFiberScope.js index 2b1d9eb27eeb..83c7f9abf61f 100644 --- a/packages/react-reconciler/src/ReactFiberScope.js +++ b/packages/react-reconciler/src/ReactFiberScope.js @@ -14,11 +14,14 @@ import type { ReactScopeMethods, } from 'shared/ReactTypes'; +import {getPublicInstance} from './ReactFiberHostConfig'; + import { HostComponent, SuspenseComponent, ScopeComponent, } from 'shared/ReactWorkTags'; +import {enableScopeAPI} from 'shared/ReactFeatureFlags'; function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean { return fiber.tag === SuspenseComponent && fiber.memoizedState !== null; @@ -33,19 +36,21 @@ function collectScopedNodes( fn: (type: string | Object, props: Object) => boolean, scopedNodes: Array, ): void { - if (node.tag === HostComponent) { - const {type, memoizedProps} = node; - if (fn(type, memoizedProps) === true) { - scopedNodes.push(node.stateNode); + if (enableScopeAPI) { + if (node.tag === HostComponent) { + const {type, memoizedProps} = node; + if (fn(type, memoizedProps) === true) { + scopedNodes.push(getPublicInstance(node.stateNode)); + } } - } - let child = node.child; + let child = node.child; - if (isFiberSuspenseAndTimedOut(node)) { - child = getSuspenseFallbackChild(node); - } - if (child !== null) { - collectScopedNodesFromChildren(child, fn, scopedNodes); + if (isFiberSuspenseAndTimedOut(node)) { + child = getSuspenseFallbackChild(node); + } + if (child !== null) { + collectScopedNodesFromChildren(child, fn, scopedNodes); + } } } diff --git a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js index 003dbce09b83..6665081f5fad 100644 --- a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js @@ -164,5 +164,185 @@ describe('ReactScope', () => { const aChildren = refA.current.getChildren(); expect(aChildren).toEqual([refC.current]); }); + + it('scopes support server-side rendering and hydration', () => { + const TestScope = React.unstable_createScope((type, props) => true); + const ReactDOMServer = require('react-dom/server'); + const scopeRef = React.createRef(); + const divRef = React.createRef(); + const spanRef = React.createRef(); + const aRef = React.createRef(); + + function Test({toggle}) { + return ( +
+ +
DIV
+ SPAN + A +
+
Outside content!
+
+ ); + } + const html = ReactDOMServer.renderToString(); + expect(html).toBe( + '
DIV
SPANA
Outside content!
', + ); + container.innerHTML = html; + ReactDOM.hydrate(, container); + const nodes = scopeRef.current.getScopedNodes(); + expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]); + }); + }); + + describe('ReactTestRenderer', () => { + let ReactTestRenderer; + + beforeEach(() => { + ReactTestRenderer = require('react-test-renderer'); + }); + + it('getScopedNodes() works as intended', () => { + const TestScope = React.unstable_createScope((type, props) => true); + const scopeRef = React.createRef(); + const divRef = React.createRef(); + const spanRef = React.createRef(); + const aRef = React.createRef(); + + function Test({toggle}) { + return toggle ? ( + +
DIV
+ SPAN + A +
+ ) : ( + + A +
DIV
+ SPAN +
+ ); + } + + const renderer = ReactTestRenderer.create(, { + createNodeMock: element => { + return element; + }, + }); + let nodes = scopeRef.current.getScopedNodes(); + expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]); + renderer.update(); + nodes = scopeRef.current.getScopedNodes(); + expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]); + }); + + it('mixed getParent() and getScopedNodes() works as intended', () => { + const TestScope = React.unstable_createScope((type, props) => true); + const TestScope2 = React.unstable_createScope((type, props) => true); + const refA = React.createRef(); + const refB = React.createRef(); + const refC = React.createRef(); + const refD = React.createRef(); + const spanA = React.createRef(); + const spanB = React.createRef(); + const divA = React.createRef(); + const divB = React.createRef(); + + function Test() { + return ( +
+ + + +
+ + + +
>Hello world
+
+
+
+
+
+
+
+
+ ); + } + + ReactTestRenderer.create(, { + createNodeMock: element => { + return element; + }, + }); + const dParent = refD.current.getParent(); + expect(dParent).not.toBe(null); + expect(dParent.getScopedNodes()).toEqual([ + divA.current, + spanB.current, + divB.current, + ]); + const cParent = refC.current.getParent(); + expect(cParent).not.toBe(null); + expect(cParent.getScopedNodes()).toEqual([ + spanA.current, + divA.current, + spanB.current, + divB.current, + ]); + expect(refB.current.getParent()).toBe(null); + expect(refA.current.getParent()).toBe(null); + }); + + it('getChildren() works as intended', () => { + const TestScope = React.unstable_createScope((type, props) => true); + const TestScope2 = React.unstable_createScope((type, props) => true); + const refA = React.createRef(); + const refB = React.createRef(); + const refC = React.createRef(); + const refD = React.createRef(); + const spanA = React.createRef(); + const spanB = React.createRef(); + const divA = React.createRef(); + const divB = React.createRef(); + + function Test() { + return ( +
+ + + +
+ + + +
>Hello world
+
+
+
+
+
+
+
+
+ ); + } + + ReactTestRenderer.create(, { + createNodeMock: element => { + return element; + }, + }); + const dChildren = refD.current.getChildren(); + expect(dChildren).toBe(null); + const cChildren = refC.current.getChildren(); + expect(cChildren).toBe(null); + const bChildren = refB.current.getChildren(); + expect(bChildren).toEqual([refD.current]); + const aChildren = refA.current.getChildren(); + expect(aChildren).toEqual([refC.current]); + }); }); }); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index f5133c528464..7ec4cc6eb125 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -37,6 +37,7 @@ import { MemoComponent, SimpleMemoComponent, IncompleteClassComponent, + ScopeComponent, } from 'shared/ReactWorkTags'; import invariant from 'shared/invariant'; import ReactVersion from 'shared/ReactVersion'; @@ -203,6 +204,7 @@ function toTree(node: ?Fiber) { case ForwardRef: case MemoComponent: case IncompleteClassComponent: + case ScopeComponent: return childrenToTree(node.child); default: invariant( diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 43cd383a4251..608385059998 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -26,7 +26,7 @@ export const warnAboutDeprecatedSetNativeProps = false; export const disableJavaScriptURLs = false; export const enableFlareAPI = true; export const enableFundamentalAPI = false; -export const enableScopeAPI = false; +export const enableScopeAPI = true; export const enableJSXTransformAPI = true; export const warnAboutUnmockedScheduler = true; export const flushSuspenseFallbacksInTests = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index edefc9ed40cf..aa860183b15e 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -73,7 +73,7 @@ export const enableFlareAPI = true; export const enableFundamentalAPI = false; -export const enableScopeAPI = false; +export const enableScopeAPI = true; export const enableJSXTransformAPI = true; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 2c22194fc219..5946ec50069e 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -340,5 +340,6 @@ "339": "An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder().", "340": "Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.", "341": "We just came from a parent so we must have had a parent. This is a bug in React.", - "342": "A React component suspended while rendering, but no fallback UI was specified.\n\nAdd a component higher in the tree to provide a loading indicator or placeholder to display." + "342": "A React component suspended while rendering, but no fallback UI was specified.\n\nAdd a component higher in the tree to provide a loading indicator or placeholder to display.", + "343": "ReactDOMServer does not yet support scope components." }