From bd79be9b687156067416ffe5219e49a11bd0f1e7 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 29 Aug 2019 12:06:51 +0100 Subject: [PATCH] [react-core] Add experimental React Scope component API (#16587) --- .../src/events/DOMEventResponderSystem.js | 86 +-------- packages/react-events/README.md | 5 - packages/react-reconciler/src/ReactFiber.js | 27 +++ .../src/ReactFiberBeginWork.js | 25 +++ .../src/ReactFiberCommitWork.js | 14 ++ .../src/ReactFiberCompleteWork.js | 33 +++- .../react-reconciler/src/ReactFiberScope.js | 130 ++++++++++++++ .../src/__tests__/ReactScope-test.internal.js | 168 ++++++++++++++++++ packages/react/src/React.js | 6 + packages/shared/ReactDOMTypes.js | 1 - packages/shared/ReactFeatureFlags.js | 3 + packages/shared/ReactSymbols.js | 1 + packages/shared/ReactTypes.js | 16 ++ packages/shared/ReactWorkTags.js | 4 +- packages/shared/createScope.js | 23 +++ .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.persistent.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 2 + packages/shared/isValidElementType.js | 4 +- 22 files changed, 459 insertions(+), 94 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberScope.js create mode 100644 packages/react-reconciler/src/__tests__/ReactScope-test.internal.js create mode 100644 packages/shared/createScope.js diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 267b9ce63b62..0544b24cec71 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -12,7 +12,7 @@ import { PASSIVE_NOT_SUPPORTED, } from 'legacy-events/EventSystemFlags'; import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; -import {HostComponent, SuspenseComponent} from 'shared/ReactWorkTags'; +import {HostComponent} from 'shared/ReactWorkTags'; import type {EventPriority} from 'shared/ReactTypes'; import type { ReactDOMEventResponder, @@ -238,28 +238,6 @@ const eventResponderContext: ReactDOMResponderContext = { } } }, - getFocusableElementsInScope(deep: boolean): Array { - validateResponderContext(); - const focusableElements = []; - const eventResponderInstance = ((currentInstance: any): ReactDOMEventResponderInstance); - const currentResponder = eventResponderInstance.responder; - let focusScopeFiber = eventResponderInstance.fiber; - if (deep) { - let deepNode = focusScopeFiber.return; - while (deepNode !== null) { - if (doesFiberHaveResponder(deepNode, currentResponder)) { - focusScopeFiber = deepNode; - } - deepNode = deepNode.return; - } - } - const child = focusScopeFiber.child; - - if (child !== null) { - collectFocusableElements(child, focusableElements); - } - return focusableElements; - }, getActiveDocument, objectAssign: Object.assign, getTimeStamp(): number { @@ -335,33 +313,6 @@ function validateEventValue(eventValue: any): void { } } -function collectFocusableElements( - node: Fiber, - focusableElements: Array, -): void { - if (isFiberSuspenseAndTimedOut(node)) { - const fallbackChild = getSuspenseFallbackChild(node); - if (fallbackChild !== null) { - collectFocusableElements(fallbackChild, focusableElements); - } - } else { - if (isFiberHostComponentFocusable(node)) { - focusableElements.push(node.stateNode); - } else { - const child = node.child; - - if (child !== null) { - collectFocusableElements(child, focusableElements); - } - } - } - const sibling = node.sibling; - - if (sibling !== null) { - collectFocusableElements(sibling, focusableElements); - } -} - function doesFiberHaveResponder( fiber: Fiber, responder: ReactDOMEventResponder, @@ -382,33 +333,6 @@ function getActiveDocument(): Document { return ((currentDocument: any): Document); } -function isFiberHostComponentFocusable(fiber: Fiber): boolean { - if (fiber.tag !== HostComponent) { - return false; - } - const {type, memoizedProps} = fiber; - if (memoizedProps.tabIndex === -1 || memoizedProps.disabled) { - return false; - } - if (memoizedProps.tabIndex === 0 || memoizedProps.contentEditable === true) { - return true; - } - if (type === 'a' || type === 'area') { - return !!memoizedProps.href && memoizedProps.rel !== 'ignore'; - } - if (type === 'input') { - return memoizedProps.type !== 'hidden' && memoizedProps.type !== 'file'; - } - return ( - type === 'button' || - type === 'textarea' || - type === 'object' || - type === 'select' || - type === 'iframe' || - type === 'embed' - ); -} - function processTimers( timers: Map, delay: number, @@ -626,14 +550,6 @@ function validateResponderContext(): void { ); } -function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean { - return fiber.tag === SuspenseComponent && fiber.memoizedState !== null; -} - -function getSuspenseFallbackChild(fiber: Fiber): Fiber | null { - return ((((fiber.child: any): Fiber).sibling: any): Fiber).child; -} - export function dispatchEventForResponderEventSystem( topLevelType: string, targetFiber: null | Fiber, diff --git a/packages/react-events/README.md b/packages/react-events/README.md index 1338d152cb23..25d68d870aaa 100644 --- a/packages/react-events/README.md +++ b/packages/react-events/README.md @@ -92,11 +92,6 @@ const event = { type: 'press', target, pointerType, x, y }; context.dispatchEvent('onPress', event, DiscreteEvent); ``` -### getFocusableElementsInScope(): Array - -Returns every DOM element that can be focused within the scope of the Event -Responder instance. - ### isTargetWithinNode(target: Element, element: Element): boolean Returns `true` if `target` is a child of `element`. diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 485edb72f33b..1b9feda2625e 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -15,6 +15,7 @@ import type { ReactEventResponder, ReactEventResponderInstance, ReactFundamentalComponent, + ReactScope, } from 'shared/ReactTypes'; import type {RootTag} from 'shared/ReactRootTags'; import type {WorkTag} from 'shared/ReactWorkTags'; @@ -32,6 +33,7 @@ import { enableProfilerTimer, enableFundamentalAPI, enableUserTimingAPI, + enableScopeAPI, } from 'shared/ReactFeatureFlags'; import {NoEffect, Placement} from 'shared/ReactSideEffectTags'; import {ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags'; @@ -56,6 +58,7 @@ import { SimpleMemoComponent, LazyComponent, FundamentalComponent, + ScopeComponent, } from 'shared/ReactWorkTags'; import getComponentName from 'shared/getComponentName'; @@ -86,6 +89,7 @@ import { REACT_MEMO_TYPE, REACT_LAZY_TYPE, REACT_FUNDAMENTAL_TYPE, + REACT_SCOPE_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -674,6 +678,16 @@ export function createFiberFromTypeAndProps( ); } break; + case REACT_SCOPE_TYPE: + if (enableScopeAPI) { + return createFiberFromScope( + type, + pendingProps, + mode, + expirationTime, + key, + ); + } } } let info = ''; @@ -766,6 +780,19 @@ export function createFiberFromFundamental( return fiber; } +function createFiberFromScope( + scope: ReactScope, + pendingProps: any, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +) { + const fiber = createFiber(ScopeComponent, pendingProps, key, mode); + fiber.type = scope; + fiber.expirationTime = expirationTime; + return fiber; +} + function createFiberFromProfiler( pendingProps: any, mode: TypeOfMode, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 4ece9c03d25c..c75a4cebec35 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -41,6 +41,7 @@ import { LazyComponent, IncompleteClassComponent, FundamentalComponent, + ScopeComponent, } from 'shared/ReactWorkTags'; import { NoEffect, @@ -63,6 +64,7 @@ import { enableSuspenseServerRenderer, enableFundamentalAPI, warnAboutDefaultPropsOnFunctionComponents, + enableScopeAPI, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import shallowEqual from 'shared/shallowEqual'; @@ -2672,6 +2674,19 @@ function updateFundamentalComponent( return workInProgress.child; } +function updateScopeComponent(current, workInProgress, renderExpirationTime) { + const nextProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; +} + export function markWorkInProgressReceivedUpdate() { didReceiveUpdate = true; } @@ -3158,6 +3173,16 @@ function beginWork( } break; } + case ScopeComponent: { + if (enableScopeAPI) { + return updateScopeComponent( + current, + workInProgress, + renderExpirationTime, + ); + } + break; + } } invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 072e0fa8c8ed..b289a4061406 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -32,6 +32,7 @@ import { enableFlareAPI, enableFundamentalAPI, enableSuspenseCallback, + enableScopeAPI, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -49,6 +50,7 @@ import { SimpleMemoComponent, SuspenseListComponent, FundamentalComponent, + ScopeComponent, } from 'shared/ReactWorkTags'; import { invokeGuardedCallback, @@ -617,6 +619,7 @@ function commitLifeCycles( case SuspenseListComponent: case IncompleteClassComponent: case FundamentalComponent: + case ScopeComponent: return; default: { invariant( @@ -691,6 +694,10 @@ function commitAttachRef(finishedWork: Fiber) { default: instanceToUse = instance; } + // Moved outside to ensure DCE works with this flag + if (enableScopeAPI && finishedWork.tag === ScopeComponent) { + instanceToUse = instance.methods; + } if (typeof ref === 'function') { ref(instanceToUse); } else { @@ -1383,6 +1390,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } return; } + case ScopeComponent: { + if (enableScopeAPI) { + const scopeInstance = finishedWork.stateNode; + scopeInstance.fiber = finishedWork; + } + return; + } default: { invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index f77eb30bf6aa..8f9addc35a18 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -9,7 +9,10 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; -import type {ReactFundamentalComponentInstance} from 'shared/ReactTypes'; +import type { + ReactFundamentalComponentInstance, + ReactScopeInstance, +} from 'shared/ReactTypes'; import type {FiberRoot} from './ReactFiberRoot'; import type { Instance, @@ -47,6 +50,7 @@ import { LazyComponent, IncompleteClassComponent, FundamentalComponent, + ScopeComponent, } from 'shared/ReactWorkTags'; import {NoMode, BatchedMode} from './ReactTypeOfMode'; import { @@ -112,6 +116,7 @@ import { enableSuspenseServerRenderer, enableFlareAPI, enableFundamentalAPI, + enableScopeAPI, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -123,6 +128,7 @@ import {createFundamentalStateInstance} from './ReactFiberFundamental'; import {Never} from './ReactFiberExpirationTime'; import {resetChildFibers} from './ReactChildFiber'; import {updateEventListeners} from './ReactFiberEvents'; +import {createScopeMethods} from './ReactFiberScope'; function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -1213,6 +1219,31 @@ function completeWork( } break; } + case ScopeComponent: { + if (enableScopeAPI) { + if (current === null) { + const type = workInProgress.type; + const scopeInstance: ReactScopeInstance = { + fiber: workInProgress, + methods: null, + }; + workInProgress.stateNode = scopeInstance; + scopeInstance.methods = createScopeMethods(type, scopeInstance); + if (workInProgress.ref !== null) { + markRef(workInProgress); + markUpdate(workInProgress); + } + } else { + if (current.ref !== workInProgress.ref) { + markRef(workInProgress); + } + if (workInProgress.ref !== null) { + markUpdate(workInProgress); + } + } + } + break; + } default: invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberScope.js b/packages/react-reconciler/src/ReactFiberScope.js new file mode 100644 index 000000000000..2b1d9eb27eeb --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberScope.js @@ -0,0 +1,130 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Fiber} from './ReactFiber'; +import type { + ReactScope, + ReactScopeInstance, + ReactScopeMethods, +} from 'shared/ReactTypes'; + +import { + HostComponent, + SuspenseComponent, + ScopeComponent, +} from 'shared/ReactWorkTags'; + +function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean { + return fiber.tag === SuspenseComponent && fiber.memoizedState !== null; +} + +function getSuspenseFallbackChild(fiber: Fiber): Fiber | null { + return ((((fiber.child: any): Fiber).sibling: any): Fiber).child; +} + +function collectScopedNodes( + node: Fiber, + 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); + } + } + let child = node.child; + + if (isFiberSuspenseAndTimedOut(node)) { + child = getSuspenseFallbackChild(node); + } + if (child !== null) { + collectScopedNodesFromChildren(child, fn, scopedNodes); + } +} + +function collectScopedNodesFromChildren( + startingChild: Fiber, + fn: (type: string | Object, props: Object) => boolean, + scopedNodes: Array, +): void { + let child = startingChild; + while (child !== null) { + collectScopedNodes(child, fn, scopedNodes); + child = child.sibling; + } +} + +function collectNearestScopeMethods( + node: Fiber, + scope: ReactScope, + childrenScopes: Array, +): void { + if (node.tag === ScopeComponent && node.type === scope) { + childrenScopes.push(node.stateNode.methods); + } else { + let child = node.child; + + if (isFiberSuspenseAndTimedOut(node)) { + child = getSuspenseFallbackChild(node); + } + if (child !== null) { + collectNearestChildScopeMethods(child, scope, childrenScopes); + } + } +} + +function collectNearestChildScopeMethods( + startingChild: Fiber, + scope: ReactScope, + childrenScopes: Array, +): void { + let child = startingChild; + while (child !== null) { + collectNearestScopeMethods(child, scope, childrenScopes); + child = child.sibling; + } +} + +export function createScopeMethods( + scope: ReactScope, + instance: ReactScopeInstance, +): ReactScopeMethods { + const fn = scope.fn; + return { + getChildren(): null | Array { + const currentFiber = ((instance.fiber: any): Fiber); + const child = currentFiber.child; + const childrenScopes = []; + if (child !== null) { + collectNearestChildScopeMethods(child, scope, childrenScopes); + } + return childrenScopes.length === 0 ? null : childrenScopes; + }, + getParent(): null | ReactScopeMethods { + let node = ((instance.fiber: any): Fiber).return; + while (node !== null) { + if (node.tag === ScopeComponent && node.type === scope) { + return node.stateNode.methods; + } + node = node.return; + } + return null; + }, + getScopedNodes(): null | Array { + const currentFiber = ((instance.fiber: any): Fiber); + const child = currentFiber.child; + const scopedNodes = []; + if (child !== null) { + collectScopedNodesFromChildren(child, fn, scopedNodes); + } + return scopedNodes.length === 0 ? null : scopedNodes; + }, + }; +} diff --git a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js new file mode 100644 index 000000000000..003dbce09b83 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js @@ -0,0 +1,168 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactFeatureFlags; + +describe('ReactScope', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableScopeAPI = true; + React = require('react'); + }); + + describe('ReactDOM', () => { + let ReactDOM; + let container; + + beforeEach(() => { + ReactDOM = require('react-dom'); + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + container = null; + }); + + 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 +
+ ); + } + + ReactDOM.render(, container); + let nodes = scopeRef.current.getScopedNodes(); + expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]); + ReactDOM.render(, container); + 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
+
+
+
+
+
+
+
+
+ ); + } + + ReactDOM.render(, container); + 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
+
+
+
+
+
+
+
+
+ ); + } + + ReactDOM.render(, container); + 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/src/React.js b/packages/react/src/React.js index 09f64fe97826..112ff1620b37 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -53,10 +53,12 @@ import { import ReactSharedInternals from './ReactSharedInternals'; import createFundamental from 'shared/createFundamentalComponent'; import createResponder from 'shared/createEventResponder'; +import createScope from 'shared/createScope'; import { enableJSXTransformAPI, enableFlareAPI, enableFundamentalAPI, + enableScopeAPI, } from 'shared/ReactFeatureFlags'; const React = { Children: { @@ -114,6 +116,10 @@ if (enableFundamentalAPI) { React.unstable_createFundamental = createFundamental; } +if (enableScopeAPI) { + React.unstable_createScope = createScope; +} + // Note: some APIs are added with feature flags. // Make sure that stable builds for open source // don't modify the React object to avoid deopts. diff --git a/packages/shared/ReactDOMTypes.js b/packages/shared/ReactDOMTypes.js index 6f2946341bb7..3519b830cfd9 100644 --- a/packages/shared/ReactDOMTypes.js +++ b/packages/shared/ReactDOMTypes.js @@ -66,7 +66,6 @@ export type ReactDOMResponderContext = { removeRootEventTypes: (rootEventTypes: Array) => void, setTimeout: (func: () => void, timeout: number) => number, clearTimeout: (timerId: number) => void, - getFocusableElementsInScope(deep: boolean): Array, getActiveDocument(): Document, objectAssign: Function, getTimeStamp: () => number, diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 3b64033a6e2f..4121f9b6d7ef 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -65,6 +65,9 @@ export const enableFlareAPI = false; // Experimental Host Component support. export const enableFundamentalAPI = false; +// Experimental Scope support. +export const enableScopeAPI = false; + // New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107 export const enableJSXTransformAPI = false; diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index 250c5f76bad8..faf5cadf1b9c 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -57,6 +57,7 @@ export const REACT_FUNDAMENTAL_TYPE = hasSymbol export const REACT_RESPONDER_TYPE = hasSymbol ? Symbol.for('react.responder') : 0xead6; +export const REACT_SCOPE_TYPE = hasSymbol ? Symbol.for('react.scope') : 0xead7; const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; const FAUX_ITERATOR_SYMBOL = '@@iterator'; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 939872036cb8..7d6d5a75cbf8 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -159,3 +159,19 @@ export type ReactFundamentalComponent = {| $$typeof: Symbol | number, impl: ReactFundamentalImpl, |}; + +export type ReactScope = {| + $$typeof: Symbol | number, + fn: (type: string | Object, props: Object) => boolean, +|}; + +export type ReactScopeMethods = {| + getChildren(): null | Array, + getParent(): null | ReactScopeMethods, + getScopedNodes(): null | Array, +|}; + +export type ReactScopeInstance = {| + fiber: Object, + methods: null | ReactScopeMethods, +|}; diff --git a/packages/shared/ReactWorkTags.js b/packages/shared/ReactWorkTags.js index 5c36488a0922..ba21171df62d 100644 --- a/packages/shared/ReactWorkTags.js +++ b/packages/shared/ReactWorkTags.js @@ -28,7 +28,8 @@ export type WorkTag = | 17 | 18 | 19 - | 20; + | 20 + | 21; export const FunctionComponent = 0; export const ClassComponent = 1; @@ -51,3 +52,4 @@ export const IncompleteClassComponent = 17; export const DehydratedFragment = 18; export const SuspenseListComponent = 19; export const FundamentalComponent = 20; +export const ScopeComponent = 21; diff --git a/packages/shared/createScope.js b/packages/shared/createScope.js new file mode 100644 index 000000000000..36c0a700bcba --- /dev/null +++ b/packages/shared/createScope.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @flow + */ + +import type {ReactScope} from 'shared/ReactTypes'; +import {REACT_SCOPE_TYPE} from 'shared/ReactSymbols'; + +export default function createScope( + fn: (type: string | Object, props: Object) => boolean, +): ReactScope { + const scopeComponent = { + $$typeof: REACT_SCOPE_TYPE, + fn, + }; + if (__DEV__) { + Object.freeze(scopeComponent); + } + return scopeComponent; +} diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 9dbed7ab7f98..c29f0c210a4d 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -33,6 +33,7 @@ export const warnAboutDeprecatedLifecycles = true; export const warnAboutDeprecatedSetNativeProps = true; export const enableFlareAPI = false; export const enableFundamentalAPI = false; +export const enableScopeAPI = false; export const enableJSXTransformAPI = false; export const warnAboutUnmockedScheduler = true; export const flushSuspenseFallbacksInTests = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 4201b41bd266..876063c2c928 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -28,6 +28,7 @@ export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; export const enableFundamentalAPI = false; +export const enableScopeAPI = false; export const enableJSXTransformAPI = false; export const warnAboutUnmockedScheduler = false; export const flushSuspenseFallbacksInTests = true; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 23d936b7eb50..dc6540b5d819 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -28,6 +28,7 @@ export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; export const enableFundamentalAPI = false; +export const enableScopeAPI = false; export const enableJSXTransformAPI = false; export const warnAboutUnmockedScheduler = true; export const flushSuspenseFallbacksInTests = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index c308b949db36..7aa962fa46e9 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -28,6 +28,7 @@ export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; export const enableFundamentalAPI = false; +export const enableScopeAPI = false; export const enableJSXTransformAPI = false; export const warnAboutUnmockedScheduler = false; export const flushSuspenseFallbacksInTests = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index b452e018ef45..43cd383a4251 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -26,6 +26,7 @@ export const warnAboutDeprecatedSetNativeProps = false; export const disableJavaScriptURLs = false; export const enableFlareAPI = true; export const enableFundamentalAPI = false; +export const enableScopeAPI = false; 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 8965f4d42155..edefc9ed40cf 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -73,6 +73,8 @@ export const enableFlareAPI = true; export const enableFundamentalAPI = false; +export const enableScopeAPI = false; + export const enableJSXTransformAPI = true; export const warnAboutUnmockedScheduler = true; diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index 92203a3c8ce2..4995077dcf68 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -21,6 +21,7 @@ import { REACT_LAZY_TYPE, REACT_FUNDAMENTAL_TYPE, REACT_RESPONDER_TYPE, + REACT_SCOPE_TYPE, } from 'shared/ReactSymbols'; export default function isValidElementType(type: mixed) { @@ -42,6 +43,7 @@ export default function isValidElementType(type: mixed) { type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || type.$$typeof === REACT_FUNDAMENTAL_TYPE || - type.$$typeof === REACT_RESPONDER_TYPE)) + type.$$typeof === REACT_RESPONDER_TYPE || + type.$$typeof === REACT_SCOPE_TYPE)) ); }