diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 07e94cd720ed1..34c3e43b81edc 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -25,6 +25,7 @@ import { SimpleMemoComponent, ContextProvider, ForwardRef, + Chunk, } from 'shared/ReactWorkTags'; type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher; @@ -623,7 +624,8 @@ export function inspectHooksOfFiber( if ( fiber.tag !== FunctionComponent && fiber.tag !== SimpleMemoComponent && - fiber.tag !== ForwardRef + fiber.tag !== ForwardRef && + fiber.tag !== Chunk ) { throw new Error( 'Unknown Fiber. Needs to be a function component to inspect hooks.', diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index eabd303dc3ab6..bd226d7a25d07 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -19,6 +19,7 @@ import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, REACT_PORTAL_TYPE, + REACT_CHUNK_TYPE, } from 'shared/ReactSymbols'; import { FunctionComponent, @@ -26,9 +27,10 @@ import { HostText, HostPortal, Fragment, + Chunk, } from 'shared/ReactWorkTags'; import invariant from 'shared/invariant'; -import {warnAboutStringRefs} from 'shared/ReactFeatureFlags'; +import {warnAboutStringRefs, enableChunksAPI} from 'shared/ReactFeatureFlags'; import { createWorkInProgress, @@ -392,32 +394,47 @@ function ChildReconciler(shouldTrackSideEffects) { element: ReactElement, expirationTime: ExpirationTime, ): Fiber { - if ( - current !== null && - (current.elementType === element.type || + if (current !== null) { + if ( + current.elementType === element.type || // Keep this check inline so it only runs on the false path: - (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)) - ) { - // Move based on index - const existing = useFiber(current, element.props, expirationTime); - existing.ref = coerceRef(returnFiber, current, element); - existing.return = returnFiber; - if (__DEV__) { - existing._debugSource = element._source; - existing._debugOwner = element._owner; + (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false) + ) { + // Move based on index + const existing = useFiber(current, element.props, expirationTime); + existing.ref = coerceRef(returnFiber, current, element); + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } else if ( + enableChunksAPI && + current.tag === Chunk && + element.type.$$typeof === REACT_CHUNK_TYPE && + element.type.render === current.type.render + ) { + // Same as above but also update the .type field. + const existing = useFiber(current, element.props, expirationTime); + existing.return = returnFiber; + existing.type = element.type; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; } - return existing; - } else { - // Insert - const created = createFiberFromElement( - element, - returnFiber.mode, - expirationTime, - ); - created.ref = coerceRef(returnFiber, current, element); - created.return = returnFiber; - return created; } + // Insert + const created = createFiberFromElement( + element, + returnFiber.mode, + expirationTime, + ); + created.ref = coerceRef(returnFiber, current, element); + created.return = returnFiber; + return created; } function updatePortal( @@ -1138,34 +1155,67 @@ function ChildReconciler(shouldTrackSideEffects) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. if (child.key === key) { - if ( - child.tag === Fragment - ? element.type === REACT_FRAGMENT_TYPE - : child.elementType === element.type || + switch (child.tag) { + case Fragment: { + if (element.type === REACT_FRAGMENT_TYPE) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber( + child, + element.props.children, + expirationTime, + ); + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } + break; + } + case Chunk: + if (enableChunksAPI) { + if ( + element.type.$$typeof === REACT_CHUNK_TYPE && + element.type.render === child.type.render + ) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, element.props, expirationTime); + existing.type = element.type; + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } + } + // We intentionally fallthrough here if enableChunksAPI is not on. + // eslint-disable-next-lined no-fallthrough + default: { + if ( + child.elementType === element.type || // Keep this check inline so it only runs on the false path: (__DEV__ ? isCompatibleFamilyForHotReloading(child, element) : false) - ) { - deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber( - child, - element.type === REACT_FRAGMENT_TYPE - ? element.props.children - : element.props, - expirationTime, - ); - existing.ref = coerceRef(returnFiber, child, element); - existing.return = returnFiber; - if (__DEV__) { - existing._debugSource = element._source; - existing._debugOwner = element._owner; + ) { + deleteRemainingChildren(returnFiber, child.sibling); + const existing = useFiber(child, element.props, expirationTime); + existing.ref = coerceRef(returnFiber, child, element); + existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } + return existing; + } + break; } - return existing; - } else { - deleteRemainingChildren(returnFiber, child); - break; } + // Didn't match. + deleteRemainingChildren(returnFiber, child); + break; } else { deleteChild(returnFiber, child); } diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 2b0152bad6648..4bc5405e0ac39 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -33,6 +33,7 @@ import { enableFundamentalAPI, enableUserTimingAPI, enableScopeAPI, + enableChunksAPI, } from 'shared/ReactFeatureFlags'; import {NoEffect, Placement} from 'shared/ReactSideEffectTags'; import {ConcurrentRoot, BlockingRoot} from 'shared/ReactRootTags'; @@ -58,6 +59,7 @@ import { LazyComponent, FundamentalComponent, ScopeComponent, + Chunk, } from 'shared/ReactWorkTags'; import getComponentName from 'shared/getComponentName'; @@ -89,6 +91,7 @@ import { REACT_LAZY_TYPE, REACT_FUNDAMENTAL_TYPE, REACT_SCOPE_TYPE, + REACT_CHUNK_TYPE, } from 'shared/ReactSymbols'; let hasBadMapPolyfill; @@ -384,6 +387,11 @@ export function resolveLazyComponentTag(Component: Function): WorkTag { if ($$typeof === REACT_MEMO_TYPE) { return MemoComponent; } + if (enableChunksAPI) { + if ($$typeof === REACT_CHUNK_TYPE) { + return Chunk; + } + } } return IndeterminateComponent; } @@ -666,6 +674,9 @@ export function createFiberFromTypeAndProps( fiberTag = LazyComponent; resolvedType = null; break getTag; + case REACT_CHUNK_TYPE: + fiberTag = Chunk; + break getTag; case REACT_FUNDAMENTAL_TYPE: if (enableFundamentalAPI) { return createFiberFromFundamental( diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index ad5d9c70798a5..e7c2fe7349b67 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -42,6 +42,7 @@ import { IncompleteClassComponent, FundamentalComponent, ScopeComponent, + Chunk, } from 'shared/ReactWorkTags'; import { NoEffect, @@ -64,6 +65,7 @@ import { enableFundamentalAPI, warnAboutDefaultPropsOnFunctionComponents, enableScopeAPI, + enableChunksAPI, } from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; import shallowEqual from 'shared/shallowEqual'; @@ -689,6 +691,82 @@ function updateFunctionComponent( return workInProgress.child; } +function updateChunk( + current: Fiber | null, + workInProgress: Fiber, + chunk: any, + nextProps: any, + renderExpirationTime: ExpirationTime, +) { + // TODO: current can be non-null here even if the component + // hasn't yet mounted. This happens after the first render suspends. + // We'll need to figure out if this is fine or can cause issues. + + const render = chunk.render; + const data = chunk.query(); + + // The rest is a fork of updateFunctionComponent + let nextChildren; + prepareToReadContext(workInProgress, renderExpirationTime); + if (__DEV__) { + ReactCurrentOwner.current = workInProgress; + setCurrentPhase('render'); + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + data, + renderExpirationTime, + ); + if ( + debugRenderPhaseSideEffectsForStrictMode && + workInProgress.mode & StrictMode + ) { + // Only double-render components with Hooks + if (workInProgress.memoizedState !== null) { + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + data, + renderExpirationTime, + ); + } + } + setCurrentPhase(null); + } else { + nextChildren = renderWithHooks( + current, + workInProgress, + render, + nextProps, + data, + renderExpirationTime, + ); + } + + if (current !== null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); + } + + // React DevTools reads this flag. + workInProgress.effectTag |= PerformedWork; + reconcileChildren( + current, + workInProgress, + nextChildren, + renderExpirationTime, + ); + return workInProgress.child; +} + function updateClassComponent( current: Fiber | null, workInProgress: Fiber, @@ -1132,6 +1210,20 @@ function mountLazyComponent( ); return child; } + case Chunk: { + if (enableChunksAPI) { + // TODO: Resolve for Hot Reloading. + child = updateChunk( + null, + workInProgress, + Component, + props, + renderExpirationTime, + ); + return child; + } + break; + } } let hint = ''; if (__DEV__) { @@ -3192,6 +3284,20 @@ function beginWork( } break; } + case Chunk: { + if (enableChunksAPI) { + const chunk = workInProgress.type; + const props = workInProgress.pendingProps; + return updateChunk( + current, + workInProgress, + chunk, + props, + renderExpirationTime, + ); + } + break; + } } invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 19926a05e2c8c..c4c3fed4e86b9 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -51,6 +51,7 @@ import { SuspenseListComponent, FundamentalComponent, ScopeComponent, + Chunk, } from 'shared/ReactWorkTags'; import { invokeGuardedCallback, @@ -247,7 +248,8 @@ function commitBeforeMutationLifeCycles( switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: - case SimpleMemoComponent: { + case SimpleMemoComponent: + case Chunk: { commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork); return; } @@ -396,7 +398,8 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: - case SimpleMemoComponent: { + case SimpleMemoComponent: + case Chunk: { commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork); commitHookEffectList(NoHookEffect, MountPassive, finishedWork); break; @@ -416,7 +419,8 @@ function commitLifeCycles( switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: - case SimpleMemoComponent: { + case SimpleMemoComponent: + case Chunk: { commitHookEffectList(UnmountLayout, MountLayout, finishedWork); return; } @@ -746,7 +750,8 @@ function commitUnmount( case FunctionComponent: case ForwardRef: case MemoComponent: - case SimpleMemoComponent: { + case SimpleMemoComponent: + case Chunk: { const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any); if (updateQueue !== null) { const lastEffect = updateQueue.lastEffect; @@ -1276,7 +1281,8 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { case FunctionComponent: case ForwardRef: case MemoComponent: - case SimpleMemoComponent: { + case SimpleMemoComponent: + case Chunk: { // Note: We currently never use MountMutation, but useLayout uses // UnmountMutation. commitHookEffectList(UnmountMutation, MountMutation, finishedWork); @@ -1315,7 +1321,8 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { case FunctionComponent: case ForwardRef: case MemoComponent: - case SimpleMemoComponent: { + case SimpleMemoComponent: + case Chunk: { // Note: We currently never use MountMutation, but useLayout uses // UnmountMutation. commitHookEffectList(UnmountMutation, MountMutation, finishedWork); diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 290b53f954226..5b3f4d5eb6c50 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -51,6 +51,7 @@ import { IncompleteClassComponent, FundamentalComponent, ScopeComponent, + Chunk, } from 'shared/ReactWorkTags'; import {NoMode, BlockingMode} from './ReactTypeOfMode'; import { @@ -118,6 +119,7 @@ import { enableDeprecatedFlareAPI, enableFundamentalAPI, enableScopeAPI, + enableChunksAPI, } from 'shared/ReactFeatureFlags'; import { markSpawnedWork, @@ -1293,6 +1295,11 @@ function completeWork( } break; } + case Chunk: + if (enableChunksAPI) { + return null; + } + break; } invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index e927b6898066c..03711d756a26e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -366,7 +366,7 @@ export function renderWithHooks( workInProgress: Fiber, Component: any, props: any, - refOrContext: any, + secondArg: any, nextRenderExpirationTime: ExpirationTime, ): any { renderExpirationTime = nextRenderExpirationTime; @@ -422,7 +422,7 @@ export function renderWithHooks( : HooksDispatcherOnUpdate; } - let children = Component(props, refOrContext); + let children = Component(props, secondArg); if (didScheduleRenderPhaseUpdate) { do { @@ -449,7 +449,7 @@ export function renderWithHooks( ? HooksDispatcherOnUpdateInDEV : HooksDispatcherOnUpdate; - children = Component(props, refOrContext); + children = Component(props, secondArg); } while (didScheduleRenderPhaseUpdate); renderPhaseUpdates = null; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index bb027d93a44a5..bbf9c96a6f906 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -87,6 +87,7 @@ import { ForwardRef, MemoComponent, SimpleMemoComponent, + Chunk, } from 'shared/ReactWorkTags'; import { NoEffect, @@ -2577,7 +2578,8 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { tag !== FunctionComponent && tag !== ForwardRef && tag !== MemoComponent && - tag !== SimpleMemoComponent + tag !== SimpleMemoComponent && + tag !== Chunk ) { // Only warn for user-defined components, not internal ones like Suspense. return; @@ -2872,7 +2874,8 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) { break; case FunctionComponent: case ForwardRef: - case SimpleMemoComponent: { + case SimpleMemoComponent: + case Chunk: { let firstHook: null | Hook = current.memoizedState; // TODO: This just checks the first Hook. Isn't it suppose to check all Hooks? if (firstHook !== null && firstHook.baseQueue !== null) { diff --git a/packages/react-reconciler/src/__tests__/ReactChunks-test.js b/packages/react-reconciler/src/__tests__/ReactChunks-test.js new file mode 100644 index 0000000000000..28ed482ebdb84 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactChunks-test.js @@ -0,0 +1,200 @@ +/** + * 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 + * @jest-environment node + */ + +let React; +let ReactNoop; +let useState; +let Suspense; +let chunk; +let readString; + +describe('ReactChunks', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + + chunk = React.chunk; + useState = React.useState; + Suspense = React.Suspense; + let cache = new Map(); + readString = function(text) { + let entry = cache.get(text); + if (!entry) { + entry = { + promise: new Promise(resolve => { + setTimeout(() => { + entry.resolved = true; + resolve(); + }, 100); + }), + resolved: false, + }; + cache.set(text, entry); + } + if (!entry.resolved) { + throw entry.promise; + } + return text; + }; + }); + + it.experimental('renders a component with a suspending query', async () => { + function Query(id) { + return { + id: id, + name: readString('Sebastian'), + }; + } + + function Render(props, data) { + return ( + + {props.title}: {data.name} + + ); + } + + let loadUser = chunk(Query, Render); + + function App({User}) { + return ( + + + + ); + } + + await ReactNoop.act(async () => { + ReactNoop.render(); + }); + + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + + await ReactNoop.act(async () => { + jest.advanceTimersByTime(1000); + }); + + expect(ReactNoop).toMatchRenderedOutput(Name: Sebastian); + }); + + it.experimental('supports a lazy wrapper around a chunk', async () => { + function Query(id) { + return { + id: id, + name: readString('Sebastian'), + }; + } + + function Render(props, data) { + return ( + + {props.title}: {data.name} + + ); + } + + let loadUser = chunk(Query, Render); + + function App({User}) { + return ( + + + + ); + } + + let resolveLazy; + let LazyUser = React.lazy( + () => + new Promise(resolve => { + resolveLazy = function() { + resolve({ + default: loadUser(123), + }); + }; + }), + ); + + await ReactNoop.act(async () => { + ReactNoop.render(); + }); + + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + + // Resolve the component. + await ReactNoop.act(async () => { + await resolveLazy(); + }); + + // We're still waiting on the data. + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + + await ReactNoop.act(async () => { + jest.advanceTimersByTime(1000); + }); + + expect(ReactNoop).toMatchRenderedOutput(Name: Sebastian); + }); + + it.experimental( + 'can receive updated data for the same component', + async () => { + function Query(firstName) { + return { + name: firstName, + }; + } + + function Render(props, data) { + let [initialName] = useState(data.name); + return ( + <> + Initial name: {initialName} + Latest name: {data.name} + + ); + } + + let loadUser = chunk(Query, Render); + + function App({User}) { + return ( + + + + ); + } + + await ReactNoop.act(async () => { + ReactNoop.render(); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> + Initial name: Sebastian + Latest name: Sebastian + , + ); + + await ReactNoop.act(async () => { + ReactNoop.render(); + }); + + expect(ReactNoop).toMatchRenderedOutput( + <> + Initial name: Sebastian + Latest name: Dan + , + ); + }, + ); +}); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 7ec4cc6eb1257..6a778f80cecaf 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -36,6 +36,7 @@ import { Profiler, MemoComponent, SimpleMemoComponent, + Chunk, IncompleteClassComponent, ScopeComponent, } from 'shared/ReactWorkTags'; @@ -185,6 +186,14 @@ function toTree(node: ?Fiber) { instance: null, rendered: childrenToTree(node.child), }; + case Chunk: + return { + nodeType: 'chunk', + type: node.type, + props: {...node.memoizedProps}, + instance: null, + rendered: childrenToTree(node.child), + }; case HostComponent: { return { nodeType: 'host', @@ -222,6 +231,7 @@ const validWrapperTypes = new Set([ ForwardRef, MemoComponent, SimpleMemoComponent, + Chunk, // Normally skipped, but used when there's more than one root child. HostRoot, ]); diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 0cf664ed8122a..e9f286a1c20fd 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -28,6 +28,7 @@ import {createContext} from './ReactContext'; import {lazy} from './ReactLazy'; import forwardRef from './forwardRef'; import memo from './memo'; +import chunk from './chunk'; import { useCallback, useContext, @@ -62,6 +63,7 @@ import { enableFundamentalAPI, enableScopeAPI, exposeConcurrentModeAPIs, + enableChunksAPI, } from 'shared/ReactFeatureFlags'; const React = { Children: { @@ -114,6 +116,10 @@ if (exposeConcurrentModeAPIs) { React.unstable_withSuspenseConfig = withSuspenseConfig; } +if (enableChunksAPI) { + React.chunk = chunk; +} + if (enableDeprecatedFlareAPI) { React.DEPRECATED_useResponder = useResponder; React.DEPRECATED_createResponder = createResponder; diff --git a/packages/react/src/chunk.js b/packages/react/src/chunk.js new file mode 100644 index 0000000000000..90bf8ac56ffdb --- /dev/null +++ b/packages/react/src/chunk.js @@ -0,0 +1,76 @@ +/** + * 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. + */ + +import { + REACT_CHUNK_TYPE, + REACT_MEMO_TYPE, + REACT_FORWARD_REF_TYPE, +} from 'shared/ReactSymbols'; + +opaque type Chunk: React$AbstractComponent< + Props, + null, +> = React$AbstractComponent; + +export default function chunk( + query: (...args: Args) => Data, + render: (props: Props, data: Data) => React$Node, +): (...args: Args) => Chunk { + if (__DEV__) { + if (typeof query !== 'function') { + console.error( + 'Chunks require a query function but was given %s.', + query === null ? 'null' : typeof query, + ); + } + if (render != null && render.$$typeof === REACT_MEMO_TYPE) { + console.error( + 'Chunks require a render function but received a `memo` ' + + 'component. Use `memo` on an inner component instead.', + ); + } else if (render != null && render.$$typeof === REACT_FORWARD_REF_TYPE) { + console.error( + 'Chunks require a render function but received a `forwardRef` ' + + 'component. Use `forwardRef` on an inner component instead.', + ); + } else if (typeof render !== 'function') { + console.error( + 'Chunks require a render function but was given %s.', + render === null ? 'null' : typeof render, + ); + } else if (render.length !== 0 && render.length !== 2) { + // Warn if it's not accepting two args. + // Do not warn for 0 arguments because it could be due to usage of the 'arguments' object + console.error( + 'Chunk render functions accept exactly two parameters: props and data. %s', + render.length === 1 + ? 'Did you forget to use the data parameter?' + : 'Any additional parameter will be undefined.', + ); + } + + if ( + render != null && + (render.defaultProps != null || render.propTypes != null) + ) { + console.error( + 'Chunk render functions do not support propTypes or defaultProps. ' + + 'Did you accidentally pass a React component?', + ); + } + } + return function(): Chunk { + let args = arguments; + return { + $$typeof: REACT_CHUNK_TYPE, + query: function() { + return query.apply(null, args); + }, + render: render, + }; + }; +} diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 6ebbd835636fd..70d46bc0734bc 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -30,6 +30,9 @@ export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = __EXPERIMENTAL__; export const enableSelectiveHydration = __EXPERIMENTAL__; +// Flight experiments +export const enableChunksAPI = __EXPERIMENTAL__; + // Only used in www builds. export const enableSchedulerDebugging = false; diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index faf5cadf1b9c2..acc990d04c93c 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -51,6 +51,7 @@ export const REACT_SUSPENSE_LIST_TYPE = hasSymbol : 0xead8; export const REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3; export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4; +export const REACT_CHUNK_TYPE = hasSymbol ? Symbol.for('react.chunk') : 0xead9; export const REACT_FUNDAMENTAL_TYPE = hasSymbol ? Symbol.for('react.fundamental') : 0xead5; diff --git a/packages/shared/ReactWorkTags.js b/packages/shared/ReactWorkTags.js index ba21171df62d0..9fa017983f816 100644 --- a/packages/shared/ReactWorkTags.js +++ b/packages/shared/ReactWorkTags.js @@ -29,7 +29,8 @@ export type WorkTag = | 18 | 19 | 20 - | 21; + | 21 + | 22; export const FunctionComponent = 0; export const ClassComponent = 1; @@ -53,3 +54,4 @@ export const DehydratedFragment = 18; export const SuspenseListComponent = 19; export const FundamentalComponent = 20; export const ScopeComponent = 21; +export const Chunk = 22; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 75beac6858661..4ebed5ecb744d 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -23,6 +23,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; +export const enableChunksAPI = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; export const warnAboutShorthandPropertyCollision = false; export const enableSchedulerDebugging = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index f53d14ee679b5..8b4b894aae2ad 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -20,6 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; +export const enableChunksAPI = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.persistent.js b/packages/shared/forks/ReactFeatureFlags.persistent.js index a92d85a784490..b4d5489328a9c 100644 --- a/packages/shared/forks/ReactFeatureFlags.persistent.js +++ b/packages/shared/forks/ReactFeatureFlags.persistent.js @@ -20,6 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; +export const enableChunksAPI = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 270b83b0483b1..848305f9ab271 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -20,6 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; +export const enableChunksAPI = false; export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 6b134a813e453..92743530e4a18 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -20,6 +20,7 @@ export const enableProfilerTimer = __PROFILE__; export const enableSchedulerTracing = __PROFILE__; export const enableSuspenseServerRenderer = false; export const enableSelectiveHydration = false; +export const enableChunksAPI = false; export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; export const enableSchedulerDebugging = false; export const disableJavaScriptURLs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 693ecabf0a252..6696c8e05ebab 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -43,6 +43,8 @@ export const exposeConcurrentModeAPIs = __EXPERIMENTAL__; export const enableSuspenseServerRenderer = true; +export const enableChunksAPI = __EXPERIMENTAL__; + export const disableJavaScriptURLs = true; let refCount = 0; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index 4c86ee094a721..83a103b68db36 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -21,6 +21,7 @@ import { REACT_SUSPENSE_TYPE, REACT_SUSPENSE_LIST_TYPE, REACT_LAZY_TYPE, + REACT_CHUNK_TYPE, } from 'shared/ReactSymbols'; import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent'; @@ -79,6 +80,8 @@ function getComponentName(type: mixed): string | null { return getWrappedName(type, type.render, 'ForwardRef'); case REACT_MEMO_TYPE: return getComponentName(type.type); + case REACT_CHUNK_TYPE: + return getComponentName(type.render); case REACT_LAZY_TYPE: { const thenable: LazyComponent = (type: any); const resolvedThenable = refineResolvedLazyComponent(thenable); diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index 4995077dcf68b..c4a6ce0d4e338 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -22,6 +22,7 @@ import { REACT_FUNDAMENTAL_TYPE, REACT_RESPONDER_TYPE, REACT_SCOPE_TYPE, + REACT_CHUNK_TYPE, } from 'shared/ReactSymbols'; export default function isValidElementType(type: mixed) { @@ -44,6 +45,7 @@ export default function isValidElementType(type: mixed) { type.$$typeof === REACT_FORWARD_REF_TYPE || type.$$typeof === REACT_FUNDAMENTAL_TYPE || type.$$typeof === REACT_RESPONDER_TYPE || - type.$$typeof === REACT_SCOPE_TYPE)) + type.$$typeof === REACT_SCOPE_TYPE || + type.$$typeof === REACT_CHUNK_TYPE)) ); }