From a62db54aa68d7e2f81885dc0e5d5b6b75f62a3e3 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 3 Jul 2019 13:50:00 +0100 Subject: [PATCH] Adds experimental fundamental interface Add flag to more areas to reduce bundle size Fix persistence mode Disable flags Add more tests and functionality Revise naming --- package.json | 1 + packages/react-art/src/ReactARTHostConfig.js | 23 +- .../src/client/ReactDOMHostConfig.js | 51 ++++ .../src/server/ReactPartialRenderer.js | 39 +++ .../src/ReactFabricHostConfig.js | 27 +-- .../src/ReactNativeHostConfig.js | 23 +- .../src/createReactNoop.js | 42 ++++ packages/react-reconciler/src/ReactFiber.js | 36 ++- .../src/ReactFiberBeginWork.js | 35 ++- .../src/ReactFiberCommitWork.js | 68 +++++- .../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 | 42 +++- 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 | 48 +++- 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, 816 insertions(+), 101 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 2a221f0e48976..a86730fc44200 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -445,27 +445,18 @@ export function unmountEventComponent( throw new Error('Not yet implemented.'); } -export function getEventTargetChildElement( - type: Symbol | number, - props: Props, -): null { +export function createFundamentalComponentInstance(eventComponentInstance) { throw new Error('Not yet implemented.'); } -export function handleEventTarget( - type: Symbol | number, - props: Props, - rootContainerInstance: Container, - internalInstanceHandle: Object, -): boolean { +export function mountFundamentalComponent(eventComponentInstance) { throw new Error('Not yet implemented.'); } -export function commitEventTarget( - type: Symbol | number, - props: Props, - instance: Instance, - parentInstance: Instance, -): void { +export function updateFundamentalComponent(eventComponentInstance) { + throw new Error('Not yet implemented.'); +} + +export function unmountFundamentalComponent(eventComponentInstance) { 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..3938e4b1d5f88 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, + ReactDOMFundamentalStateInstance, } 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,52 @@ export function unmountEventComponent( unmountEventResponder(eventComponentInstance); } } + +export function createFundamentalComponentInstance( + fundamentalInstance: ReactDOMFundamentalStateInstance, +): 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: ReactDOMFundamentalStateInstance, +): void { + if (enableFundamentalAPI) { + const {impl, instance, props, state} = fundamentalInstance; + const onMount = impl.onMount; + if (onMount !== undefined) { + onMount(null, instance, props, state); + } + } +} + +export function updateFundamentalComponent( + fundamentalInstance: ReactDOMFundamentalStateInstance, +): 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: ReactDOMFundamentalStateInstance, +): 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 fdc7e17b3aa1a..56e0dbc46583e 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 { @@ -1191,6 +1193,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 52a3966f2d715..0c9743d13412e 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -475,27 +475,22 @@ export function unmountEventComponent( } } -export function getEventTargetChildElement( - type: Symbol | number, - props: Props, -): null { +export function createFundamentalComponentInstance(eventComponentInstance) { throw new Error('Not yet implemented.'); } -export function handleEventTarget( - type: Symbol | number, - props: Props, - rootContainerInstance: Container, - internalInstanceHandle: Object, -): boolean { +export function mountFundamentalComponent(eventComponentInstance) { throw new Error('Not yet implemented.'); } -export function commitEventTarget( - type: Symbol | number, - props: Props, - instance: Instance, - parentInstance: Instance, -): void { +export function updateFundamentalComponent(eventComponentInstance) { + throw new Error('Not yet implemented.'); +} + +export function unmountFundamentalComponent(eventComponentInstance) { + throw new Error('Not yet implemented.'); +} + +export function cloneFundamentalInstance(eventComponentInstance) { 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 28d7317f3ff1f..4511cb3214de5 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -514,27 +514,18 @@ export function unmountEventComponent( throw new Error('Not yet implemented.'); } -export function getEventTargetChildElement( - type: Symbol | number, - props: Props, -): null { +export function createFundamentalComponentInstance(eventComponentInstance) { throw new Error('Not yet implemented.'); } -export function handleEventTarget( - type: Symbol | number, - props: Props, - rootContainerInstance: Container, - internalInstanceHandle: Object, -): boolean { +export function mountFundamentalComponent(eventComponentInstance) { throw new Error('Not yet implemented.'); } -export function commitEventTarget( - type: Symbol | number, - props: Props, - instance: Instance, - parentInstance: Instance, -): void { +export function updateFundamentalComponent(eventComponentInstance) { + throw new Error('Not yet implemented.'); +} + +export function unmountFundamentalComponent(eventComponentInstance) { 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..b2cc2cfe9e84d 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -407,6 +407,48 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { unmountEventComponent(): void { // NO-OP }, + + createFundamentalComponentInstance(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); + } + }, + + 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 bbaede6bf5919..a48e9e9c27c22 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} 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; @@ -662,6 +669,17 @@ export function createFiberFromTypeAndProps( ); } break; + case REACT_FUNDAMENTAL_TYPE: + if (enableFundamentalAPI) { + return createFiberFromFundamental( + type, + pendingProps, + mode, + expirationTime, + key, + ); + } + break; } } let info = ''; @@ -741,7 +759,7 @@ export function createFiberFromFragment( } export function createFiberFromEventComponent( - eventComponent: ReactEventComponent, + eventComponent: ReactEventComponent, pendingProps: any, mode: TypeOfMode, expirationTime: ExpirationTime, @@ -754,6 +772,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 1be03dfd2719e..ee1ff28d5213d 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'; @@ -2412,7 +2414,7 @@ function updateContextConsumer( function updateEventComponent(current, workInProgress, renderExpirationTime) { const nextProps = workInProgress.pendingProps; - let nextChildren = nextProps.children; + const nextChildren = nextProps.children; reconcileChildren( current, @@ -2424,6 +2426,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; } @@ -2913,6 +2936,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..d952589794073 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,14 @@ function commitUnmount(current: Fiber): void { unmountEventComponent(eventComponentInstance); current.stateNode = null; } + break; + } + case FundamentalComponent: { + if (enableFundamentalAPI) { + const fundamentalInstance = current.stateNode; + unmountFundamentalComponent(fundamentalInstance); + current.stateNode = null; + } } } } @@ -838,6 +851,7 @@ function commitContainer(finishedWork: Fiber) { case ClassComponent: case HostComponent: case HostText: + case FundamentalComponent: case EventComponent: { return; } @@ -942,20 +956,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 +995,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 +1056,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 +1097,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 +1286,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 534dbf7959413..08319a5c77275 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, + ReactFundamentalStateInstance, +} 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,9 @@ import { appendChildToContainerChildSet, finalizeContainerChildren, updateEventComponent, + createFundamentalComponentInstance, + mountFundamentalComponent, + cloneFundamentalInstance, } from './ReactFiberHostConfig'; import { getRootHostContainer, @@ -109,6 +116,7 @@ import { enableSchedulerTracing, enableSuspenseServerRenderer, enableFlareAPI, + enableFundamentalAPI, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -120,6 +128,7 @@ import { getEventComponentHostChildrenCount, createEventComponentInstance, } from './ReactFiberEvents'; +import {createFundamentalStateInstance} from './ReactFiberFundamental'; import getComponentName from 'shared/getComponentName'; import warning from 'shared/warning'; import {Never} from './ReactFiberExpirationTime'; @@ -154,6 +163,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 @@ -263,6 +274,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 @@ -348,6 +368,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 @@ -1131,7 +1160,7 @@ function completeWork( workInProgress.stateNode; if (eventComponentInstance === null) { - let responderState = null; + let responderState; if (__DEV__ && !responder.allowMultipleHostChildren) { const hostChildrenCount = getEventComponentHostChildrenCount( workInProgress, @@ -1142,8 +1171,9 @@ function completeWork( getComponentName(workInProgress.type), ); } - if (responder.createInitialState !== undefined) { - responderState = responder.createInitialState(newProps); + const createInitialState = responder.createInitialState; + if (createInitialState !== undefined) { + responderState = createInitialState(newProps); } eventComponentInstance = workInProgress.stateNode = createEventComponentInstance( workInProgress, @@ -1164,6 +1194,51 @@ function completeWork( } break; } + case FundamentalComponent: { + if (enableFundamentalAPI) { + const fundamentalImpl = workInProgress.type.impl; + let fundamentalInstance: ReactFundamentalStateInstance< + 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 = ((createFundamentalComponentInstance( + 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 + fundamentalInstance.prevProps = fundamentalInstance.props; + fundamentalInstance.props = newProps; + fundamentalInstance.currentFiber = workInProgress; + if (supportsPersistence) { + const instance = cloneFundamentalInstance(fundamentalInstance); + fundamentalInstance.instance = instance; + appendAllChildren(instance, workInProgress, false, false); + } + 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..31e15c90321dd --- /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, + ReactFundamentalStateInstance, +} from 'shared/ReactTypes'; + +export function createFundamentalStateInstance( + currentFiber: Fiber, + props: Object, + impl: ReactFundamentalImpl, + state: Object, +): ReactFundamentalStateInstance { + 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 be4bab8089bfe..916ae3acb2ca2 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 421ed5f625dcb..0f8791c26f2e3 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -65,9 +65,10 @@ 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 getEventTargetChildElement = - $$$hostConfig.getEventTargetChildElement; +export const createFundamentalComponentInstance = + $$$hostConfig.createFundamentalComponentInstance; +export const mountFundamentalComponent = + $$$hostConfig.mountFundamentalComponent; // ------------------- // Mutation @@ -89,6 +90,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 @@ -103,6 +108,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 512cd0eb0ef28..b8fce85e81a22 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, + ReactFundamentalStateInstance, +} from 'shared/ReactTypes'; import {enableFlareAPI} from 'shared/ReactFeatureFlags'; @@ -302,3 +305,40 @@ export function unmountEventComponent( ): void { // noop } + +export function createFundamentalComponentInstance( + fundamentalInstance, +): Instance { + const {impl, props, state} = fundamentalInstance; + return impl.getInstance(null, props, state); +} + +export function mountFundamentalComponent( + fundamentalInstance: ReactFundamentalStateInstance, +): void { + const {impl, instance, props, state} = fundamentalInstance; + const onMount = impl.onMount; + if (onMount !== undefined) { + onMount(null, instance, props, state); + } +} + +export function updateFundamentalComponent( + fundamentalInstance: ReactFundamentalStateInstance, +): 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: ReactFundamentalStateInstance, +): 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 4b3bb797646be..69be5e718c28a 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -53,7 +53,12 @@ import { import ReactSharedInternals from './ReactSharedInternals'; import {error, warn} from './withComponentStack'; 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, @@ -109,6 +114,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 5f14ecf2b00db..c7350855f7a44 100644 --- a/packages/shared/ReactDOMTypes.js +++ b/packages/shared/ReactDOMTypes.js @@ -8,6 +8,7 @@ */ import type { + ReactFundamentalStateInstance, ReactEventResponder, ReactEventComponentInstance, EventPriority, @@ -49,6 +50,11 @@ export type ReactDOMEventComponentInstance = ReactEventComponentInstance< ReactDOMResponderContext, >; +export type ReactDOMFundamentalStateInstance = ReactFundamentalStateInstance< + 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 02bc0e7fbe2e5..025cdaea237b5 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -14,8 +14,7 @@ export type ReactNode = | ReactFragment | ReactProvider | ReactConsumer - | ReactEventComponent - | ReactEventTarget; + | ReactEventComponent; export type ReactEmpty = null | void | boolean; @@ -111,14 +110,47 @@ export type ReactEventComponent = {| responder: ReactEventResponder, |}; -export type ReactEventTarget = {| - $$typeof: Symbol | number, - displayName?: string, - type: Symbol | number, -|}; - export opaque type EventPriority = 0 | 1 | 2; export const DiscreteEvent: EventPriority = 0; export const UserBlockingEvent: EventPriority = 1; export const ContinuousEvent: EventPriority = 2; + +export type ReactFundamentalStateInstance = {| + 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, + 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 faea980806c3b..aca08e98e3ee7 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 1e1ec36101841..fee89bbf11073 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." }