[react-core] Add more support for experimental React Scope API (#16621)

This commit is contained in:
Dominic Gannaway
2019-08-30 18:27:14 +01:00
committed by GitHub
parent af7d58ffe9
commit 6a3f5d63d1
7 changed files with 229 additions and 14 deletions
+27
View File
@@ -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.',
);
}
}
}
+16 -11
View File
@@ -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<any>,
): 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);
}
}
}
@@ -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>
<TestScope ref={scopeRef}>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
<a ref={aRef}>A</a>
</TestScope>
<div>Outside content!</div>
</div>
);
}
const html = ReactDOMServer.renderToString(<Test />);
expect(html).toBe(
'<div data-reactroot=""><div>DIV</div><span>SPAN</span><a>A</a><div>Outside content!</div></div>',
);
container.innerHTML = html;
ReactDOM.hydrate(<Test />, 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 ? (
<TestScope ref={scopeRef}>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
<a ref={aRef}>A</a>
</TestScope>
) : (
<TestScope ref={scopeRef}>
<a ref={aRef}>A</a>
<div ref={divRef}>DIV</div>
<span ref={spanRef}>SPAN</span>
</TestScope>
);
}
const renderer = ReactTestRenderer.create(<Test toggle={true} />, {
createNodeMock: element => {
return element;
},
});
let nodes = scopeRef.current.getScopedNodes();
expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]);
renderer.update(<Test toggle={false} />);
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 (
<div>
<TestScope ref={refA}>
<span ref={spanA}>
<TestScope2 ref={refB}>
<div ref={divA}>
<TestScope ref={refC}>
<span ref={spanB}>
<TestScope2 ref={refD}>
<div ref={divB}>>Hello world</div>
</TestScope2>
</span>
</TestScope>
</div>
</TestScope2>
</span>
</TestScope>
</div>
);
}
ReactTestRenderer.create(<Test />, {
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 (
<div>
<TestScope ref={refA}>
<span ref={spanA}>
<TestScope2 ref={refB}>
<div ref={divA}>
<TestScope ref={refC}>
<span ref={spanB}>
<TestScope2 ref={refD}>
<div ref={divB}>>Hello world</div>
</TestScope2>
</span>
</TestScope>
</div>
</TestScope2>
</span>
</TestScope>
</div>
);
}
ReactTestRenderer.create(<Test />, {
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]);
});
});
});
+2
View File
@@ -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(
@@ -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;
@@ -73,7 +73,7 @@ export const enableFlareAPI = true;
export const enableFundamentalAPI = false;
export const enableScopeAPI = false;
export const enableScopeAPI = true;
export const enableJSXTransformAPI = true;
+2 -1
View File
@@ -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 <Suspense fallback=...> 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 <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.",
"343": "ReactDOMServer does not yet support scope components."
}