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))
);
}