From 2c4d61e1022ae383dd11fe237f6df8451e6f0310 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 19 Jul 2019 22:20:28 +0100 Subject: [PATCH] Adds experimental fundamental interface (#16049) --- package.json | 1 + packages/react-art/src/ReactARTHostConfig.js | 20 ++ .../src/client/ReactDOMHostConfig.js | 64 +++++ .../src/server/ReactPartialRenderer.js | 39 +++ .../src/ReactFabricHostConfig.js | 24 ++ .../src/ReactNativeHostConfig.js | 20 ++ .../src/createReactNoop.js | 51 ++++ packages/react-reconciler/src/ReactFiber.js | 36 ++- .../src/ReactFiberBeginWork.js | 35 ++- .../src/ReactFiberCommitWork.js | 70 +++++- .../src/ReactFiberCompleteWork.js | 83 ++++++- .../src/ReactFiberFundamental.js | 30 +++ .../src/ReactFiberReconciler.js | 4 + .../src/ReactFiberTreeReflection.js | 7 +- .../ReactFiberFundamental-test.internal.js | 225 ++++++++++++++++++ .../src/forks/ReactFiberHostConfig.custom.js | 12 +- .../src/ReactTestHostConfig.js | 51 +++- packages/react/src/React.js | 11 +- .../shared/HostConfigWithNoPersistence.js | 1 + packages/shared/ReactDOMTypes.js | 6 + packages/shared/ReactFeatureFlags.js | 3 + packages/shared/ReactSymbols.js | 3 + packages/shared/ReactTypes.js | 45 ++++ packages/shared/ReactWorkTags.js | 2 +- packages/shared/createEventComponent.js | 22 +- packages/shared/createFundamentalComponent.js | 33 +++ .../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/hasBadMapPolyfill.js | 26 ++ packages/shared/isValidElementType.js | 4 +- scripts/error-codes/codes.json | 3 +- 35 files changed, 898 insertions(+), 40 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberFundamental.js create mode 100644 packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js create mode 100644 packages/shared/createFundamentalComponent.js create mode 100644 packages/shared/hasBadMapPolyfill.js diff --git a/package.json b/package.json index 85411e430e955..13b23b070a0bb 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "debug-test": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source.js --runInBand", "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js", "test-persistent": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source-persistent.js", + "debug-test-persistent": "cross-env NODE_ENV=development node --inspect-brk node_modules/.bin/jest --config ./scripts/jest/config.source-persistent.js --runInBand", "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js", "test-prod-build": "yarn test-build-prod", "test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js", diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 58580f1f5ca85..11df9690f7fd4 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -444,3 +444,23 @@ export function unmountEventComponent( ): void { throw new Error('Not yet implemented.'); } + +export function getFundamentalComponentInstance(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function mountFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function shouldUpdateFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function updateFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function unmountFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index f76d77cb0ac3b..901b2413b9550 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -45,6 +45,7 @@ import type {DOMContainer} from './ReactDOM'; import type { ReactDOMEventResponder, ReactDOMEventComponentInstance, + ReactDOMFundamentalComponentInstance, } from 'shared/ReactDOMTypes'; import { addRootEventTypesForComponentInstance, @@ -103,6 +104,7 @@ export type NoTimeout = -1; import { enableSuspenseServerRenderer, enableFlareAPI, + enableFundamentalAPI, } from 'shared/ReactFeatureFlags'; import warning from 'shared/warning'; @@ -882,3 +884,65 @@ export function unmountEventComponent( unmountEventResponder(eventComponentInstance); } } + +export function getFundamentalComponentInstance( + fundamentalInstance: ReactDOMFundamentalComponentInstance, +): Instance { + if (enableFundamentalAPI) { + const {currentFiber, impl, props, state} = fundamentalInstance; + const instance = impl.getInstance(null, props, state); + precacheFiberNode(currentFiber, instance); + return instance; + } + // Because of the flag above, this gets around the Flow error; + return (null: any); +} + +export function mountFundamentalComponent( + fundamentalInstance: ReactDOMFundamentalComponentInstance, +): void { + if (enableFundamentalAPI) { + const {impl, instance, props, state} = fundamentalInstance; + const onMount = impl.onMount; + if (onMount !== undefined) { + onMount(null, instance, props, state); + } + } +} + +export function shouldUpdateFundamentalComponent( + fundamentalInstance: ReactDOMFundamentalComponentInstance, +): boolean { + if (enableFundamentalAPI) { + const {impl, prevProps, props, state} = fundamentalInstance; + const shouldUpdate = impl.shouldUpdate; + if (shouldUpdate !== undefined) { + return shouldUpdate(null, prevProps, props, state); + } + } + return true; +} + +export function updateFundamentalComponent( + fundamentalInstance: ReactDOMFundamentalComponentInstance, +): void { + if (enableFundamentalAPI) { + const {impl, instance, prevProps, props, state} = fundamentalInstance; + const onUpdate = impl.onUpdate; + if (onUpdate !== undefined) { + onUpdate(null, instance, prevProps, props, state); + } + } +} + +export function unmountFundamentalComponent( + fundamentalInstance: ReactDOMFundamentalComponentInstance, +): void { + if (enableFundamentalAPI) { + const {impl, instance, props, state} = fundamentalInstance; + const onUnmount = impl.onUnmount; + if (onUnmount !== undefined) { + onUnmount(null, instance, props, state); + } + } +} diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 34c95ec25cc24..99dfaa5bd1e46 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -23,6 +23,7 @@ import { warnAboutDeprecatedLifecycles, enableSuspenseServerRenderer, enableFlareAPI, + enableFundamentalAPI, } from 'shared/ReactFeatureFlags'; import { @@ -39,6 +40,7 @@ import { REACT_LAZY_TYPE, REACT_MEMO_TYPE, REACT_EVENT_COMPONENT_TYPE, + REACT_FUNDAMENTAL_TYPE, } from 'shared/ReactSymbols'; import { @@ -1190,6 +1192,43 @@ class ReactDOMServerRenderer { ); } // eslint-disable-next-line-no-fallthrough + case REACT_FUNDAMENTAL_TYPE: { + if (enableFundamentalAPI) { + const fundamentalImpl = elementType.impl; + const open = fundamentalImpl.getServerSideString( + null, + nextElement.props, + ); + const getServerSideStringClose = + fundamentalImpl.getServerSideStringClose; + const close = + getServerSideStringClose !== undefined + ? getServerSideStringClose(null, nextElement.props) + : ''; + const nextChildren = + fundamentalImpl.reconcileChildren !== false + ? toArray(((nextChild: any): ReactElement).props.children) + : []; + const frame: Frame = { + type: null, + domNamespace: parentNamespace, + children: nextChildren, + childIndex: 0, + context: context, + footer: close, + }; + if (__DEV__) { + ((frame: any): FrameDev).debugElementStack = []; + } + this.stack.push(frame); + return open; + } + invariant( + false, + 'ReactDOMServer does not yet support the fundamental API.', + ); + } + // eslint-disable-next-line-no-fallthrough case REACT_LAZY_TYPE: invariant( false, diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 49a26bc870fb4..dc2572ff6d32c 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -472,3 +472,27 @@ export function unmountEventComponent( unmountEventResponder(eventComponentInstance); } } + +export function getFundamentalComponentInstance(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function mountFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function shouldUpdateFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function updateFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function unmountFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function cloneFundamentalInstance(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index fd8ba518ad534..1dd7a3afac1ca 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -511,3 +511,23 @@ export function unmountEventComponent( ): void { throw new Error('Not yet implemented.'); } + +export function getFundamentalComponentInstance(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function mountFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function shouldUpdateFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function updateFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} + +export function unmountFundamentalComponent(fundamentalInstance) { + throw new Error('Not yet implemented.'); +} diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 5033283025e2c..46e3fd3183657 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -407,6 +407,57 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { unmountEventComponent(): void { // NO-OP }, + + getFundamentalComponentInstance(fundamentalInstance): Instance { + const {impl, props, state} = fundamentalInstance; + return impl.getInstance(null, props, state); + }, + + mountFundamentalComponent(fundamentalInstance): void { + const {impl, instance, props, state} = fundamentalInstance; + const onMount = impl.onUpdate; + if (onMount !== undefined) { + onMount(null, instance, props, state); + } + }, + + shouldUpdateFundamentalComponent(fundamentalInstance): boolean { + const {impl, instance, prevProps, props, state} = fundamentalInstance; + const shouldUpdate = impl.shouldUpdate; + if (shouldUpdate !== undefined) { + return shouldUpdate(null, instance, prevProps, props, state); + } + return true; + }, + + updateFundamentalComponent(fundamentalInstance): void { + const {impl, instance, prevProps, props, state} = fundamentalInstance; + const onUpdate = impl.onUpdate; + if (onUpdate !== undefined) { + onUpdate(null, instance, prevProps, props, state); + } + }, + + unmountFundamentalComponent(fundamentalInstance): void { + const {impl, instance, props, state} = fundamentalInstance; + const onUnmount = impl.onUnmount; + if (onUnmount !== undefined) { + onUnmount(null, instance, props, state); + } + }, + + cloneFundamentalInstance(fundamentalInstance): Instance { + const instance = fundamentalInstance.instance; + return { + children: [], + text: instance.text, + type: instance.type, + prop: instance.prop, + id: instance.id, + context: instance.context, + hidden: instance.hidden, + }; + }, }; const hostConfig = useMutation diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 73a3c4c1794ef..13c452fb52a39 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -13,6 +13,7 @@ import type { ReactPortal, RefObject, ReactEventComponent, + ReactFundamentalComponent, } from 'shared/ReactTypes'; import type {RootTag} from 'shared/ReactRootTags'; import type {WorkTag} from 'shared/ReactWorkTags'; @@ -26,7 +27,11 @@ import type {ReactEventComponentInstance} from 'shared/ReactTypes'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; -import {enableProfilerTimer, enableFlareAPI} from 'shared/ReactFeatureFlags'; +import { + enableProfilerTimer, + enableFlareAPI, + enableFundamentalAPI, +} from 'shared/ReactFeatureFlags'; import {NoEffect, Placement} from 'shared/ReactSideEffectTags'; import {ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags'; import { @@ -49,6 +54,7 @@ import { SimpleMemoComponent, LazyComponent, EventComponent, + FundamentalComponent, } from 'shared/ReactWorkTags'; import getComponentName from 'shared/getComponentName'; @@ -79,6 +85,7 @@ import { REACT_MEMO_TYPE, REACT_LAZY_TYPE, REACT_EVENT_COMPONENT_TYPE, + REACT_FUNDAMENTAL_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -663,6 +670,17 @@ export function createFiberFromTypeAndProps( ); } break; + case REACT_FUNDAMENTAL_TYPE: + if (enableFundamentalAPI) { + return createFiberFromFundamental( + type, + pendingProps, + mode, + expirationTime, + key, + ); + } + break; } } let info = ''; @@ -742,7 +760,7 @@ export function createFiberFromFragment( } export function createFiberFromEventComponent( - eventComponent: ReactEventComponent, + eventComponent: ReactEventComponent, pendingProps: any, mode: TypeOfMode, expirationTime: ExpirationTime, @@ -755,6 +773,20 @@ export function createFiberFromEventComponent( return fiber; } +export function createFiberFromFundamental( + fundamentalComponent: ReactFundamentalComponent, + pendingProps: any, + mode: TypeOfMode, + expirationTime: ExpirationTime, + key: null | string, +): Fiber { + const fiber = createFiber(FundamentalComponent, pendingProps, key, mode); + fiber.elementType = fundamentalComponent; + fiber.type = fundamentalComponent; + 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 3c62caf0990bb..df2519a248274 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -42,6 +42,7 @@ import { LazyComponent, IncompleteClassComponent, EventComponent, + FundamentalComponent, } from 'shared/ReactWorkTags'; import { NoEffect, @@ -61,6 +62,7 @@ import { enableSchedulerTracing, enableSuspenseServerRenderer, enableFlareAPI, + enableFundamentalAPI, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import shallowEqual from 'shared/shallowEqual'; @@ -2479,7 +2481,7 @@ function updateContextConsumer( function updateEventComponent(current, workInProgress, renderExpirationTime) { const nextProps = workInProgress.pendingProps; - let nextChildren = nextProps.children; + const nextChildren = nextProps.children; reconcileChildren( current, @@ -2491,6 +2493,27 @@ function updateEventComponent(current, workInProgress, renderExpirationTime) { return workInProgress.child; } +function updateFundamentalComponent( + current, + workInProgress, + renderExpirationTime, +) { + const fundamentalImpl = workInProgress.type.impl; + if (fundamentalImpl.reconcileChildren === false) { + return null; + } + const nextProps = workInProgress.pendingProps; + const nextChildren = nextProps.children; + + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; +} + export function markWorkInProgressReceivedUpdate() { didReceiveUpdate = true; } @@ -2980,6 +3003,16 @@ function beginWork( } break; } + case FundamentalComponent: { + if (enableFundamentalAPI) { + return updateFundamentalComponent( + current, + workInProgress, + renderExpirationTime, + ); + } + break; + } } invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 576ac65f27838..bf5591af1bba5 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -29,6 +29,7 @@ import { enableProfilerTimer, enableSuspenseServerRenderer, enableFlareAPI, + enableFundamentalAPI, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -46,6 +47,7 @@ import { SimpleMemoComponent, EventComponent, SuspenseListComponent, + FundamentalComponent, } from 'shared/ReactWorkTags'; import { invokeGuardedCallback, @@ -94,6 +96,8 @@ import { unhideTextInstance, unmountEventComponent, mountEventComponent, + unmountFundamentalComponent, + updateFundamentalComponent, } from './ReactFiberHostConfig'; import { captureCommitPhaseError, @@ -590,6 +594,7 @@ function commitLifeCycles( case SuspenseComponent: case SuspenseListComponent: case IncompleteClassComponent: + case FundamentalComponent: return; case EventComponent: { if (enableFlareAPI) { @@ -757,6 +762,16 @@ function commitUnmount(current: Fiber): void { unmountEventComponent(eventComponentInstance); current.stateNode = null; } + break; + } + case FundamentalComponent: { + if (enableFundamentalAPI) { + const fundamentalInstance = current.stateNode; + if (fundamentalInstance !== null) { + unmountFundamentalComponent(fundamentalInstance); + current.stateNode = null; + } + } } } } @@ -838,6 +853,7 @@ function commitContainer(finishedWork: Fiber) { case ClassComponent: case HostComponent: case HostText: + case FundamentalComponent: case EventComponent: { return; } @@ -942,20 +958,26 @@ function commitPlacement(finishedWork: Fiber): void { // Note: these two variables *must* always be updated together. let parent; let isContainer; - + const parentStateNode = parentFiber.stateNode; switch (parentFiber.tag) { case HostComponent: - parent = parentFiber.stateNode; + parent = parentStateNode; isContainer = false; break; case HostRoot: - parent = parentFiber.stateNode.containerInfo; + parent = parentStateNode.containerInfo; isContainer = true; break; case HostPortal: - parent = parentFiber.stateNode.containerInfo; + parent = parentStateNode.containerInfo; isContainer = true; break; + case FundamentalComponent: + if (enableFundamentalAPI) { + parent = parentStateNode.instance; + isContainer = false; + } + // eslint-disable-next-line-no-fallthrough default: invariant( false, @@ -975,8 +997,9 @@ function commitPlacement(finishedWork: Fiber): void { // children to find all the terminal nodes. let node: Fiber = finishedWork; while (true) { - if (node.tag === HostComponent || node.tag === HostText) { - const stateNode = node.stateNode; + const isHost = node.tag === HostComponent || node.tag === HostText; + if (isHost || node.tag === FundamentalComponent) { + const stateNode = isHost ? node.stateNode : node.stateNode.instance; if (before) { if (isContainer) { insertInContainerBefore(parent, stateNode, before); @@ -1035,19 +1058,25 @@ function unmountHostComponents(current): void { 'Expected to find a host parent. This error is likely caused by ' + 'a bug in React. Please file an issue.', ); + const parentStateNode = parent.stateNode; switch (parent.tag) { case HostComponent: - currentParent = parent.stateNode; + currentParent = parentStateNode; currentParentIsContainer = false; break findParent; case HostRoot: - currentParent = parent.stateNode.containerInfo; + currentParent = parentStateNode.containerInfo; currentParentIsContainer = true; break findParent; case HostPortal: - currentParent = parent.stateNode.containerInfo; + currentParent = parentStateNode.containerInfo; currentParentIsContainer = true; break findParent; + case FundamentalComponent: + if (enableFundamentalAPI) { + currentParent = parentStateNode.instance; + currentParentIsContainer = false; + } } parent = parent.return; } @@ -1070,6 +1099,22 @@ function unmountHostComponents(current): void { ); } // Don't visit children because we already visited them. + } else if (node.tag === FundamentalComponent) { + const fundamentalNode = node.stateNode.instance; + commitNestedUnmounts(node); + // After all the children have unmounted, it is now safe to remove the + // node from the tree. + if (currentParentIsContainer) { + removeChildFromContainer( + ((currentParent: any): Container), + (fundamentalNode: Instance), + ); + } else { + removeChild( + ((currentParent: any): Instance), + (fundamentalNode: Instance), + ); + } } else if ( enableSuspenseServerRenderer && node.tag === DehydratedSuspenseComponent @@ -1243,6 +1288,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { case EventComponent: { return; } + case FundamentalComponent: { + if (enableFundamentalAPI) { + const fundamentalInstance = finishedWork.stateNode; + updateFundamentalComponent(fundamentalInstance); + } + return; + } default: { invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index faa65481200dd..5609ce15717fd 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -17,7 +17,10 @@ import type { Container, ChildSet, } from './ReactFiberHostConfig'; -import type {ReactEventComponentInstance} from 'shared/ReactTypes'; +import type { + ReactEventComponentInstance, + ReactFundamentalComponentInstance, +} from 'shared/ReactTypes'; import type { SuspenseState, SuspenseListRenderState, @@ -48,6 +51,7 @@ import { LazyComponent, IncompleteClassComponent, EventComponent, + FundamentalComponent, } from 'shared/ReactWorkTags'; import {NoMode, BatchedMode} from './ReactTypeOfMode'; import { @@ -75,6 +79,10 @@ import { appendChildToContainerChildSet, finalizeContainerChildren, updateEventComponent, + getFundamentalComponentInstance, + mountFundamentalComponent, + cloneFundamentalInstance, + shouldUpdateFundamentalComponent, } from './ReactFiberHostConfig'; import { getRootHostContainer, @@ -109,6 +117,7 @@ import { enableSchedulerTracing, enableSuspenseServerRenderer, enableFlareAPI, + enableFundamentalAPI, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -117,6 +126,7 @@ import { renderHasNotSuspendedYet, } from './ReactFiberWorkLoop'; import {createEventComponentInstance} from './ReactFiberEvents'; +import {createFundamentalStateInstance} from './ReactFiberFundamental'; import {Never} from './ReactFiberExpirationTime'; import {resetChildFibers} from './ReactChildFiber'; @@ -149,6 +159,8 @@ if (supportsMutation) { while (node !== null) { if (node.tag === HostComponent || node.tag === HostText) { appendInitialChild(parent, node.stateNode); + } else if (node.tag === FundamentalComponent) { + appendInitialChild(parent, node.stateNode.instance); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in @@ -258,6 +270,15 @@ if (supportsMutation) { instance = cloneHiddenTextInstance(instance, text, node); } appendInitialChild(parent, instance); + } else if (enableFundamentalAPI && node.tag === FundamentalComponent) { + let instance = node.stateNode.instance; + if (needsVisibilityToggle && isHidden) { + // This child is inside a timed out tree. Hide it. + const props = node.memoizedProps; + const type = node.type; + instance = cloneHiddenInstance(instance, type, props, node); + } + appendInitialChild(parent, instance); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in @@ -343,6 +364,15 @@ if (supportsMutation) { instance = cloneHiddenTextInstance(instance, text, node); } appendChildToContainerChildSet(containerChildSet, instance); + } else if (enableFundamentalAPI && node.tag === FundamentalComponent) { + let instance = node.stateNode.instance; + if (needsVisibilityToggle && isHidden) { + // This child is inside a timed out tree. Hide it. + const props = node.memoizedProps; + const type = node.type; + instance = cloneHiddenInstance(instance, type, props, node); + } + appendChildToContainerChildSet(containerChildSet, instance); } else if (node.tag === HostPortal) { // If we have a portal child, then we don't want to traverse // down its children. Instead, we'll get insertions from each child in @@ -1139,6 +1169,57 @@ function completeWork( } break; } + case FundamentalComponent: { + if (enableFundamentalAPI) { + const fundamentalImpl = workInProgress.type.impl; + let fundamentalInstance: ReactFundamentalComponentInstance< + any, + any, + > | null = + workInProgress.stateNode; + + if (fundamentalInstance === null) { + const getInitialState = fundamentalImpl.getInitialState; + let fundamentalState; + if (getInitialState !== undefined) { + fundamentalState = getInitialState(newProps); + } + fundamentalInstance = workInProgress.stateNode = createFundamentalStateInstance( + workInProgress, + newProps, + fundamentalImpl, + fundamentalState || {}, + ); + const instance = ((getFundamentalComponentInstance( + fundamentalInstance, + ): any): Instance); + fundamentalInstance.instance = instance; + if (fundamentalImpl.reconcileChildren === false) { + return null; + } + appendAllChildren(instance, workInProgress, false, false); + mountFundamentalComponent(fundamentalInstance); + } else { + // We fire update in commit phase + const prevProps = fundamentalInstance.props; + fundamentalInstance.prevProps = prevProps; + fundamentalInstance.props = newProps; + fundamentalInstance.currentFiber = workInProgress; + if (supportsPersistence) { + const instance = cloneFundamentalInstance(fundamentalInstance); + fundamentalInstance.instance = instance; + appendAllChildren(instance, workInProgress, false, false); + } + const shouldUpdate = shouldUpdateFundamentalComponent( + fundamentalInstance, + ); + if (shouldUpdate) { + markUpdate(workInProgress); + } + } + } + break; + } default: invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberFundamental.js b/packages/react-reconciler/src/ReactFiberFundamental.js new file mode 100644 index 0000000000000..ba2d6fd22327c --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberFundamental.js @@ -0,0 +1,30 @@ +/** + * 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 { + ReactFundamentalImpl, + ReactFundamentalComponentInstance, +} from 'shared/ReactTypes'; + +export function createFundamentalStateInstance( + currentFiber: Fiber, + props: Object, + impl: ReactFundamentalImpl, + state: Object, +): ReactFundamentalComponentInstance { + return { + currentFiber, + impl, + instance: null, + prevProps: null, + props, + state, + }; +} diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 682bfe7d3bf39..aade7ccb15d3b 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -16,6 +16,7 @@ import type { Container, PublicInstance, } from './ReactFiberHostConfig'; +import {FundamentalComponent} from 'shared/ReactWorkTags'; import type {ReactNodeList} from 'shared/ReactTypes'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; @@ -374,6 +375,9 @@ export function findHostInstanceWithNoPortals( if (hostFiber === null) { return null; } + if (hostFiber.tag === FundamentalComponent) { + return hostFiber.stateNode.instance; + } return hostFiber.stateNode; } diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index 734dead68cf6f..abcb1f36776b4 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -21,6 +21,7 @@ import { HostRoot, HostPortal, HostText, + FundamentalComponent, } from 'shared/ReactWorkTags'; import {NoEffect, Placement} from 'shared/ReactSideEffectTags'; @@ -277,7 +278,11 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null { // Next we'll drill down this component to find the first HostComponent/Text. let node: Fiber = currentParent; while (true) { - if (node.tag === HostComponent || node.tag === HostText) { + if ( + node.tag === HostComponent || + node.tag === HostText || + node.tag === FundamentalComponent + ) { return node; } else if (node.child && node.tag !== HostPortal) { node.child.return = node; diff --git a/packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js new file mode 100644 index 0000000000000..00b00b533b95f --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactFiberFundamental-test.internal.js @@ -0,0 +1,225 @@ +/** + * 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 ReactNoop; +let Scheduler; +let ReactFeatureFlags; +let ReactTestRenderer; +let ReactDOM; +let ReactDOMServer; + +function createReactFundamentalComponent(fundamentalImpl) { + return React.unstable_createFundamental(fundamentalImpl); +} + +function init() { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableFundamentalAPI = true; + React = require('react'); + Scheduler = require('scheduler'); +} + +function initNoopRenderer() { + init(); + ReactNoop = require('react-noop-renderer'); +} + +function initTestRenderer() { + init(); + ReactTestRenderer = require('react-test-renderer'); +} + +function initReactDOM() { + init(); + ReactDOM = require('react-dom'); +} + +function initReactDOMServer() { + init(); + ReactDOMServer = require('react-dom/server'); +} + +describe('ReactFiberFundamental', () => { + describe('NoopRenderer', () => { + beforeEach(() => { + initNoopRenderer(); + }); + + it('should render a simple fundamental component with a single child', () => { + const FundamentalComponent = createReactFundamentalComponent({ + reconcileChildren: true, + getInstance(context, props, state) { + const instance = { + children: [], + text: null, + type: 'test', + }; + return instance; + }, + }); + + const Test = ({children}) => ( + {children} + ); + + ReactNoop.render(Hello world); + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop).toMatchRenderedOutput(Hello world); + ReactNoop.render(Hello world again); + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop).toMatchRenderedOutput(Hello world again); + ReactNoop.render(null); + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop).toMatchRenderedOutput(null); + }); + }); + + describe('NoopTestRenderer', () => { + beforeEach(() => { + initTestRenderer(); + }); + + it('should render a simple fundamental component with a single child', () => { + const FundamentalComponent = createReactFundamentalComponent({ + reconcileChildren: true, + getInstance(context, props, state) { + const instance = { + children: [], + props, + type: 'test', + tag: 'INSTANCE', + }; + return instance; + }, + }); + + const Test = ({children}) => ( + {children} + ); + + const root = ReactTestRenderer.create(null); + root.update(Hello world); + expect(Scheduler).toFlushWithoutYielding(); + expect(root).toMatchRenderedOutput(Hello world); + root.update(Hello world again); + expect(Scheduler).toFlushWithoutYielding(); + expect(root).toMatchRenderedOutput(Hello world again); + root.update(null); + expect(Scheduler).toFlushWithoutYielding(); + expect(root).toMatchRenderedOutput(null); + }); + }); + + describe('ReactDOM', () => { + beforeEach(() => { + initReactDOM(); + }); + + it('should render a simple fundamental component with a single child', () => { + const FundamentalComponent = createReactFundamentalComponent({ + reconcileChildren: true, + getInstance(context, props, state) { + const instance = document.createElement('div'); + return instance; + }, + }); + + const Test = ({children}) => ( + {children} + ); + + const container = document.createElement('div'); + ReactDOM.render(Hello world, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
Hello world
'); + ReactDOM.render(Hello world again, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
Hello world again
'); + ReactDOM.render(null, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe(''); + }); + + it('should render a simple fundamental component without reconcileChildren', () => { + const FundamentalComponent = createReactFundamentalComponent({ + reconcileChildren: false, + getInstance(context, props, state) { + const instance = document.createElement('div'); + instance.textContent = 'Hello world'; + return instance; + }, + }); + + const Test = () => ; + + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
Hello world
'); + // Children should be ignored + ReactDOM.render(Hello world again, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe('
Hello world
'); + ReactDOM.render(null, container); + expect(Scheduler).toFlushWithoutYielding(); + expect(container.innerHTML).toBe(''); + }); + }); + + describe('ReactDOMServer', () => { + beforeEach(() => { + initReactDOMServer(); + }); + + it('should render a simple fundamental component with a single child', () => { + const getInstance = jest.fn(); + const FundamentalComponent = createReactFundamentalComponent({ + reconcileChildren: true, + getInstance, + getServerSideString(context, props) { + return `
`; + }, + getServerSideStringClose(context, props) { + return `
`; + }, + }); + + const Test = ({children}) => ( + {children} + ); + + expect(getInstance).not.toBeCalled(); + let output = ReactDOMServer.renderToString(Hello world); + expect(output).toBe('
Hello world
'); + output = ReactDOMServer.renderToString(Hello world again); + expect(output).toBe('
Hello world again
'); + }); + + it('should render a simple fundamental component without reconcileChildren', () => { + const FundamentalComponent = createReactFundamentalComponent({ + reconcileChildren: false, + getServerSideString(context, props) { + return `
Hello world
`; + }, + }); + + const Test = () => ; + + let output = ReactDOMServer.renderToString(); + expect(output).toBe('
Hello world
'); + // Children should be ignored + output = ReactDOMServer.renderToString(Hello world again); + expect(output).toBe('
Hello world
'); + }); + }); +}); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 5d87abd379ecc..80f5221b25f35 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -65,7 +65,12 @@ export const supportsPersistence = $$$hostConfig.supportsPersistence; export const supportsHydration = $$$hostConfig.supportsHydration; export const mountEventComponent = $$$hostConfig.mountEventComponent; export const updateEventComponent = $$$hostConfig.updateEventComponent; -export const handleEventTarget = $$$hostConfig.handleEventTarget; +export const getFundamentalComponentInstance = + $$$hostConfig.getFundamentalComponentInstance; +export const mountFundamentalComponent = + $$$hostConfig.mountFundamentalComponent; +export const shouldUpdateFundamentalComponent = + $$$hostConfig.shouldUpdateFundamentalComponent; // ------------------- // Mutation @@ -87,6 +92,10 @@ export const unhideInstance = $$$hostConfig.unhideInstance; export const unhideTextInstance = $$$hostConfig.unhideTextInstance; export const unmountEventComponent = $$$hostConfig.unmountEventComponent; export const commitEventTarget = $$$hostConfig.commitEventTarget; +export const updateFundamentalComponent = + $$$hostConfig.updateFundamentalComponent; +export const unmountFundamentalComponent = + $$$hostConfig.unmountFundamentalComponent; // ------------------- // Persistence @@ -101,6 +110,7 @@ export const finalizeContainerChildren = export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren; export const cloneHiddenInstance = $$$hostConfig.cloneHiddenInstance; export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance; +export const cloneFundamentalInstance = $$$hostConfig.cloneInstance; // ------------------- // Hydration diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index 467fc4550c335..f57f425aa89b0 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -9,7 +9,10 @@ import warning from 'shared/warning'; -import type {ReactEventComponentInstance} from 'shared/ReactTypes'; +import type { + ReactEventComponentInstance, + ReactFundamentalComponentInstance, +} from 'shared/ReactTypes'; import {enableFlareAPI} from 'shared/ReactFeatureFlags'; @@ -302,3 +305,49 @@ export function unmountEventComponent( ): void { // noop } + +export function getFundamentalComponentInstance(fundamentalInstance): Instance { + const {impl, props, state} = fundamentalInstance; + return impl.getInstance(null, props, state); +} + +export function mountFundamentalComponent( + fundamentalInstance: ReactFundamentalComponentInstance, +): void { + const {impl, instance, props, state} = fundamentalInstance; + const onMount = impl.onMount; + if (onMount !== undefined) { + onMount(null, instance, props, state); + } +} + +export function shouldUpdateFundamentalComponent( + fundamentalInstance: ReactFundamentalComponentInstance, +): boolean { + const {impl, prevProps, props, state} = fundamentalInstance; + const shouldUpdate = impl.shouldUpdate; + if (shouldUpdate !== undefined) { + return shouldUpdate(null, prevProps, props, state); + } + return true; +} + +export function updateFundamentalComponent( + fundamentalInstance: ReactFundamentalComponentInstance, +): void { + const {impl, instance, prevProps, props, state} = fundamentalInstance; + const onUpdate = impl.onUpdate; + if (onUpdate !== undefined) { + onUpdate(null, instance, prevProps, props, state); + } +} + +export function unmountFundamentalComponent( + fundamentalInstance: ReactFundamentalComponentInstance, +): void { + const {impl, instance, props, state} = fundamentalInstance; + const onUnmount = impl.onUnmount; + if (onUnmount !== undefined) { + onUnmount(null, instance, props, state); + } +} diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 74bb3a5683a90..a1a181c97633c 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -52,7 +52,12 @@ import { } from './ReactElementValidator'; import ReactSharedInternals from './ReactSharedInternals'; import createEvent from 'shared/createEventComponent'; -import {enableJSXTransformAPI, enableFlareAPI} from 'shared/ReactFeatureFlags'; +import createFundamental from 'shared/createFundamentalComponent'; +import { + enableJSXTransformAPI, + enableFlareAPI, + enableFundamentalAPI, +} from 'shared/ReactFeatureFlags'; const React = { Children: { map, @@ -105,6 +110,10 @@ if (enableFlareAPI) { React.unstable_useEvent = useEvent; } +if (enableFundamentalAPI) { + React.unstable_createFundamental = createFundamental; +} + // 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/HostConfigWithNoPersistence.js b/packages/shared/HostConfigWithNoPersistence.js index d5f84cf43fd6d..2b58a0db4d7b0 100644 --- a/packages/shared/HostConfigWithNoPersistence.js +++ b/packages/shared/HostConfigWithNoPersistence.js @@ -24,6 +24,7 @@ function shim(...args: any) { // Persistence (when unsupported) export const supportsPersistence = false; export const cloneInstance = shim; +export const cloneFundamentalInstance = shim; export const createContainerChildSet = shim; export const appendChildToContainerChildSet = shim; export const finalizeContainerChildren = shim; diff --git a/packages/shared/ReactDOMTypes.js b/packages/shared/ReactDOMTypes.js index 060a2a11d7be2..9590ec323bcb6 100644 --- a/packages/shared/ReactDOMTypes.js +++ b/packages/shared/ReactDOMTypes.js @@ -8,6 +8,7 @@ */ import type { + ReactFundamentalComponentInstance, ReactEventResponder, ReactEventComponentInstance, EventPriority, @@ -44,6 +45,11 @@ export type ReactDOMEventComponentInstance = ReactEventComponentInstance< ReactDOMResponderContext, >; +export type ReactDOMFundamentalComponentInstance = ReactFundamentalComponentInstance< + any, + any, +>; + export type ReactDOMResponderContext = { dispatchEvent: ( eventObject: Object, diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index c7c4f54bb4da2..2c87bb1a06efa 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -62,6 +62,9 @@ export const warnAboutDeprecatedSetNativeProps = false; // Experimental React Flare event system and event components support. export const enableFlareAPI = false; +// Experimental Host Component support. +export const enableFundamentalAPI = 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 77d46ad32de20..6581cc34d5cb5 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -54,6 +54,9 @@ export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4; export const REACT_EVENT_COMPONENT_TYPE = hasSymbol ? Symbol.for('react.event_component') : 0xead5; +export const REACT_FUNDAMENTAL_TYPE = hasSymbol + ? Symbol.for('react.fundamental') + : 0xead6; 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 d8ef40f36130b..58c9b6bddc08e 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -112,3 +112,48 @@ export opaque type EventPriority = 0 | 1 | 2; export const DiscreteEvent: EventPriority = 0; export const UserBlockingEvent: EventPriority = 1; export const ContinuousEvent: EventPriority = 2; + +export type ReactFundamentalComponentInstance = {| + currentFiber: mixed, + instance: mixed, + prevProps: null | Object, + props: Object, + impl: ReactFundamentalImpl, + state: Object, +|}; + +export type ReactFundamentalImpl = { + displayName: string, + reconcileChildren: boolean, + getInitialState?: (props: Object) => Object, + getInstance: (context: C, props: Object, state: Object) => H, + getServerSideString?: (context: C, props: Object) => string, + getServerSideStringClose?: (context: C, props: Object) => string, + onMount: (context: C, instance: mixed, props: Object, state: Object) => void, + shouldUpdate?: ( + context: C, + prevProps: null | Object, + nextProps: Object, + state: Object, + ) => boolean, + onUpdate?: ( + context: C, + instance: mixed, + prevProps: null | Object, + nextProps: Object, + state: Object, + ) => void, + onUnmount?: ( + context: C, + instance: mixed, + props: Object, + state: Object, + ) => void, + onHydrate?: (context: C, props: Object, state: Object) => boolean, + onFocus?: (context: C, props: Object, state: Object) => boolean, +}; + +export type ReactFundamentalComponent = {| + $$typeof: Symbol | number, + impl: ReactFundamentalImpl, +|}; diff --git a/packages/shared/ReactWorkTags.js b/packages/shared/ReactWorkTags.js index c5d397264fa0c..9de376d6910b2 100644 --- a/packages/shared/ReactWorkTags.js +++ b/packages/shared/ReactWorkTags.js @@ -51,5 +51,5 @@ export const LazyComponent = 16; export const IncompleteClassComponent = 17; export const DehydratedSuspenseComponent = 18; export const EventComponent = 19; -export const EventTarget = 20; +export const FundamentalComponent = 20; export const SuspenseListComponent = 21; diff --git a/packages/shared/createEventComponent.js b/packages/shared/createEventComponent.js index 622e9d6b2fbcb..66998b3942c7d 100644 --- a/packages/shared/createEventComponent.js +++ b/packages/shared/createEventComponent.js @@ -8,25 +8,7 @@ import type {ReactEventResponder, ReactEventComponent} from 'shared/ReactTypes'; import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols'; - -let hasBadMapPolyfill; - -if (__DEV__) { - hasBadMapPolyfill = false; - try { - const frozenObject = Object.freeze({}); - const testMap = new Map([[frozenObject, null]]); - const testSet = new Set([frozenObject]); - // This is necessary for Rollup to not consider these unused. - // https://github.com/rollup/rollup/issues/1771 - // TODO: we can remove these if Rollup fixes the bug. - testMap.set(0, 0); - testSet.add(0); - } catch (e) { - // TODO: Consider warning about bad polyfills - hasBadMapPolyfill = true; - } -} +import {hasBadMapPolyfill} from './hasBadMapPolyfill'; export default function createEventComponent( responder: ReactEventResponder, @@ -39,7 +21,7 @@ export default function createEventComponent( } const eventComponent = { $$typeof: REACT_EVENT_COMPONENT_TYPE, - responder: responder, + responder, }; if (__DEV__) { Object.freeze(eventComponent); diff --git a/packages/shared/createFundamentalComponent.js b/packages/shared/createFundamentalComponent.js new file mode 100644 index 0000000000000..88fa369922d80 --- /dev/null +++ b/packages/shared/createFundamentalComponent.js @@ -0,0 +1,33 @@ +/** + * 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 { + ReactFundamentalImpl, + ReactFundamentalComponent, +} from 'shared/ReactTypes'; +import {REACT_FUNDAMENTAL_TYPE} from 'shared/ReactSymbols'; +import {hasBadMapPolyfill} from './hasBadMapPolyfill'; + +export default function createFundamentalComponent( + impl: ReactFundamentalImpl, +): ReactFundamentalComponent { + // We use responder as a Map key later on. When we have a bad + // polyfill, then we can't use it as a key as the polyfill tries + // to add a property to the object. + if (__DEV__ && !hasBadMapPolyfill) { + Object.freeze(impl); + } + const fundamantalComponent = { + $$typeof: REACT_FUNDAMENTAL_TYPE, + impl, + }; + if (__DEV__) { + Object.freeze(fundamantalComponent); + } + return fundamantalComponent; +} diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index dd632f0cbe684..e458937a62af5 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -32,6 +32,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = true; export const warnAboutDeprecatedSetNativeProps = true; export const enableFlareAPI = false; +export const enableFundamentalAPI = false; export const enableJSXTransformAPI = false; export const warnAboutMissingMockScheduler = true; export const revertPassiveEffectsChange = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index b5049d473a0ed..25c03a53870e1 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -27,6 +27,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; +export const enableFundamentalAPI = false; export const enableJSXTransformAPI = false; export const warnAboutMissingMockScheduler = false; export const revertPassiveEffectsChange = false; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index 88e8103373c01..a43a9b687393e 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -27,6 +27,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; +export const enableFundamentalAPI = false; export const enableJSXTransformAPI = false; export const warnAboutMissingMockScheduler = true; export const revertPassiveEffectsChange = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index f71eb8275ed9b..1f50db3277307 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -27,6 +27,7 @@ export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const enableFlareAPI = false; +export const enableFundamentalAPI = false; export const enableJSXTransformAPI = false; export const warnAboutMissingMockScheduler = false; export const revertPassiveEffectsChange = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index bef8c47484f5b..060d0d814238a 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -28,6 +28,7 @@ export const enableSchedulerDebugging = false; export const warnAboutDeprecatedSetNativeProps = false; export const disableJavaScriptURLs = false; export const enableFlareAPI = true; +export const enableFundamentalAPI = false; export const enableJSXTransformAPI = true; export const warnAboutMissingMockScheduler = true; export const enableUserBlockingEvents = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 09ac6f03aad5e..a9c01b5417f82 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -70,6 +70,8 @@ function updateFlagOutsideOfReactCallStack() { export const enableFlareAPI = true; +export const enableFundamentalAPI = false; + export const enableJSXTransformAPI = true; export const warnAboutMissingMockScheduler = true; diff --git a/packages/shared/hasBadMapPolyfill.js b/packages/shared/hasBadMapPolyfill.js new file mode 100644 index 0000000000000..7ceb9ca65f9ae --- /dev/null +++ b/packages/shared/hasBadMapPolyfill.js @@ -0,0 +1,26 @@ +/** + * 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 + */ + +export let hasBadMapPolyfill; + +if (__DEV__) { + hasBadMapPolyfill = false; + try { + const frozenObject = Object.freeze({}); + const testMap = new Map([[frozenObject, null]]); + const testSet = new Set([frozenObject]); + // This is necessary for Rollup to not consider these unused. + // https://github.com/rollup/rollup/issues/1771 + // TODO: we can remove these if Rollup fixes the bug. + testMap.set(0, 0); + testSet.add(0); + } catch (e) { + // TODO: Consider warning about bad polyfills + hasBadMapPolyfill = true; + } +} diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index 739b216991b0e..0b9f768f4dc4c 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -20,6 +20,7 @@ import { REACT_MEMO_TYPE, REACT_LAZY_TYPE, REACT_EVENT_COMPONENT_TYPE, + REACT_FUNDAMENTAL_TYPE, } from 'shared/ReactSymbols'; export default function isValidElementType(type: mixed) { @@ -40,6 +41,7 @@ export default function isValidElementType(type: mixed) { type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || - type.$$typeof === REACT_EVENT_COMPONENT_TYPE)) + type.$$typeof === REACT_EVENT_COMPONENT_TYPE || + type.$$typeof === REACT_FUNDAMENTAL_TYPE)) ); } diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index caacc6085ef06..926646f116b9d 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -334,5 +334,6 @@ "333": "This should have a parent host component initialized. This error is likely caused by a bug in React. Please file an issue.", "334": "accumulate(...): Accumulated items must not be null or undefined.", "335": "ReactDOMServer does not yet support the event API.", - "336": "The \"%s\" event responder cannot be used via the \"useEvent\" hook." + "336": "The \"%s\" event responder cannot be used via the \"useEvent\" hook.", + "337": "ReactDOMServer does not yet support the fundamental API." }