Skip to content

Commit

Permalink
[react-core] Add more support for experimental React Scope API (#16621)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Aug 30, 2019
1 parent 2c1e6bf commit 46f912f
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 14 deletions.
27 changes: 27 additions & 0 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
enableSuspenseServerRenderer,
enableFundamentalAPI,
enableFlareAPI,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';

import {
Expand All @@ -48,6 +49,7 @@ import {
REACT_LAZY_TYPE,
REACT_MEMO_TYPE,
REACT_FUNDAMENTAL_TYPE,
REACT_SCOPE_TYPE,
} from 'shared/ReactSymbols';

import {
Expand Down Expand Up @@ -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.',
);
}
}
}

Expand Down
27 changes: 16 additions & 11 deletions packages/react-reconciler/src/ReactFiberScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
}

Expand Down
180 changes: 180 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactScope-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 2 additions & 0 deletions packages/react-test-renderer/src/ReactTestRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
MemoComponent,
SimpleMemoComponent,
IncompleteClassComponent,
ScopeComponent,
} from 'shared/ReactWorkTags';
import invariant from 'shared/invariant';
import ReactVersion from 'shared/ReactVersion';
Expand Down Expand Up @@ -203,6 +204,7 @@ function toTree(node: ?Fiber) {
case ForwardRef:
case MemoComponent:
case IncompleteClassComponent:
case ScopeComponent:
return childrenToTree(node.child);
default:
invariant(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/forks/ReactFeatureFlags.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const enableFlareAPI = true;

export const enableFundamentalAPI = false;

export const enableScopeAPI = false;
export const enableScopeAPI = true;

export const enableJSXTransformAPI = true;

Expand Down
3 changes: 2 additions & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}

0 comments on commit 46f912f

Please sign in to comment.