@samitouri / QOS-React-2 / commits / 7dc9745427

[Flight] Chunks API (#17398)

* Add feature flags * Add Chunk type and constructor * Wire up Chunk support in the reconciler * Update reconciler to reconcile Chunks against the render method This allows the query and args to be updated. * Drop the ref. Chunks cannot have refs anyway. * Add Chunk checks in more missing cases * Rename secondArg * Add test and fix lazy chunks Not really a supported use case but for consistency I guess. * Fix fragment test

Sebastian Markbåge committed Dec 18, 2019 at 18:25 UTC 7dc9745427046d462506e9788878ba389e176b8a
23 files changed +556 -60
packages/react-debug-tools/src/ReactDebugHooks.js
+3 -1
@@ -25,6 +25,7 @@ import {
25 SimpleMemoComponent,
26 ContextProvider,
27 ForwardRef,
28 + Chunk,
29 } from 'shared/ReactWorkTags';
30
31 type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher;
@@ -623,7 +624,8 @@ export function inspectHooksOfFiber(
624 if (
625 fiber.tag !== FunctionComponent &&
626 fiber.tag !== SimpleMemoComponent &&
626 - fiber.tag !== ForwardRef
627 + fiber.tag !== ForwardRef &&
628 + fiber.tag !== Chunk
629 ) {
630 throw new Error(
631 'Unknown Fiber. Needs to be a function component to inspect hooks.',
packages/react-reconciler/src/ReactChildFiber.js
+96 -46
@@ -19,6 +19,7 @@ import {
19 REACT_ELEMENT_TYPE,
20 REACT_FRAGMENT_TYPE,
21 REACT_PORTAL_TYPE,
22 + REACT_CHUNK_TYPE,
23 } from 'shared/ReactSymbols';
24 import {
25 FunctionComponent,
@@ -26,9 +27,10 @@ import {
27 HostText,
28 HostPortal,
29 Fragment,
30 + Chunk,
31 } from 'shared/ReactWorkTags';
32 import invariant from 'shared/invariant';
31 -import {warnAboutStringRefs} from 'shared/ReactFeatureFlags';
33 +import {warnAboutStringRefs, enableChunksAPI} from 'shared/ReactFeatureFlags';
34
35 import {
36 createWorkInProgress,
@@ -392,32 +394,47 @@ function ChildReconciler(shouldTrackSideEffects) {
394 element: ReactElement,
395 expirationTime: ExpirationTime,
396 ): Fiber {
395 - if (
396 - current !== null &&
397 - (current.elementType === element.type ||
397 + if (current !== null) {
398 + if (
399 + current.elementType === element.type ||
400 // Keep this check inline so it only runs on the false path:
399 - (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false))
400 - ) {
401 - // Move based on index
402 - const existing = useFiber(current, element.props, expirationTime);
403 - existing.ref = coerceRef(returnFiber, current, element);
404 - existing.return = returnFiber;
405 - if (__DEV__) {
406 - existing._debugSource = element._source;
407 - existing._debugOwner = element._owner;
401 + (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)
402 + ) {
403 + // Move based on index
404 + const existing = useFiber(current, element.props, expirationTime);
405 + existing.ref = coerceRef(returnFiber, current, element);
406 + existing.return = returnFiber;
407 + if (__DEV__) {
408 + existing._debugSource = element._source;
409 + existing._debugOwner = element._owner;
410 + }
411 + return existing;
412 + } else if (
413 + enableChunksAPI &&
414 + current.tag === Chunk &&
415 + element.type.$$typeof === REACT_CHUNK_TYPE &&
416 + element.type.render === current.type.render
417 + ) {
418 + // Same as above but also update the .type field.
419 + const existing = useFiber(current, element.props, expirationTime);
420 + existing.return = returnFiber;
421 + existing.type = element.type;
422 + if (__DEV__) {
423 + existing._debugSource = element._source;
424 + existing._debugOwner = element._owner;
425 + }
426 + return existing;
427 }
409 - return existing;
410 - } else {
411 - // Insert
412 - const created = createFiberFromElement(
413 - element,
414 - returnFiber.mode,
415 - expirationTime,
416 - );
417 - created.ref = coerceRef(returnFiber, current, element);
418 - created.return = returnFiber;
419 - return created;
428 }
429 + // Insert
430 + const created = createFiberFromElement(
431 + element,
432 + returnFiber.mode,
433 + expirationTime,
434 + );
435 + created.ref = coerceRef(returnFiber, current, element);
436 + created.return = returnFiber;
437 + return created;
438 }
439
440 function updatePortal(
@@ -1138,34 +1155,67 @@ function ChildReconciler(shouldTrackSideEffects) {
1155 // TODO: If key === null and child.key === null, then this only applies to
1156 // the first item in the list.
1157 if (child.key === key) {
1141 - if (
1142 - child.tag === Fragment
1143 - ? element.type === REACT_FRAGMENT_TYPE
1144 - : child.elementType === element.type ||
1158 + switch (child.tag) {
1159 + case Fragment: {
1160 + if (element.type === REACT_FRAGMENT_TYPE) {
1161 + deleteRemainingChildren(returnFiber, child.sibling);
1162 + const existing = useFiber(
1163 + child,
1164 + element.props.children,
1165 + expirationTime,
1166 + );
1167 + existing.return = returnFiber;
1168 + if (__DEV__) {
1169 + existing._debugSource = element._source;
1170 + existing._debugOwner = element._owner;
1171 + }
1172 + return existing;
1173 + }
1174 + break;
1175 + }
1176 + case Chunk:
1177 + if (enableChunksAPI) {
1178 + if (
1179 + element.type.$$typeof === REACT_CHUNK_TYPE &&
1180 + element.type.render === child.type.render
1181 + ) {
1182 + deleteRemainingChildren(returnFiber, child.sibling);
1183 + const existing = useFiber(child, element.props, expirationTime);
1184 + existing.type = element.type;
1185 + existing.return = returnFiber;
1186 + if (__DEV__) {
1187 + existing._debugSource = element._source;
1188 + existing._debugOwner = element._owner;
1189 + }
1190 + return existing;
1191 + }
1192 + }
1193 + // We intentionally fallthrough here if enableChunksAPI is not on.
1194 + // eslint-disable-next-lined no-fallthrough
1195 + default: {
1196 + if (
1197 + child.elementType === element.type ||
1198 // Keep this check inline so it only runs on the false path:
1199 (__DEV__
1200 ? isCompatibleFamilyForHotReloading(child, element)
1201 : false)
1149 - ) {
1150 - deleteRemainingChildren(returnFiber, child.sibling);
1151 - const existing = useFiber(
1152 - child,
1153 - element.type === REACT_FRAGMENT_TYPE
1154 - ? element.props.children
1155 - : element.props,
1156 - expirationTime,
1157 - );
1158 - existing.ref = coerceRef(returnFiber, child, element);
1159 - existing.return = returnFiber;
1160 - if (__DEV__) {
1161 - existing._debugSource = element._source;
1162 - existing._debugOwner = element._owner;
1202 + ) {
1203 + deleteRemainingChildren(returnFiber, child.sibling);
1204 + const existing = useFiber(child, element.props, expirationTime);
1205 + existing.ref = coerceRef(returnFiber, child, element);
1206 + existing.return = returnFiber;
1207 + if (__DEV__) {
1208 + existing._debugSource = element._source;
1209 + existing._debugOwner = element._owner;
1210 + }
1211 + return existing;
1212 + }
1213 + break;
1214 }
1164 - return existing;
1165 - } else {
1166 - deleteRemainingChildren(returnFiber, child);
1167 - break;
1215 }
1216 + // Didn't match.
1217 + deleteRemainingChildren(returnFiber, child);
1218 + break;
1219 } else {
1220 deleteChild(returnFiber, child);
1221 }
packages/react-reconciler/src/ReactFiber.js
+11
@@ -33,6 +33,7 @@ import {
33 enableFundamentalAPI,
34 enableUserTimingAPI,
35 enableScopeAPI,
36 + enableChunksAPI,
37 } from 'shared/ReactFeatureFlags';
38 import {NoEffect, Placement} from 'shared/ReactSideEffectTags';
39 import {ConcurrentRoot, BlockingRoot} from 'shared/ReactRootTags';
@@ -58,6 +59,7 @@ import {
59 LazyComponent,
60 FundamentalComponent,
61 ScopeComponent,
62 + Chunk,
63 } from 'shared/ReactWorkTags';
64 import getComponentName from 'shared/getComponentName';
65
@@ -89,6 +91,7 @@ import {
91 REACT_LAZY_TYPE,
92 REACT_FUNDAMENTAL_TYPE,
93 REACT_SCOPE_TYPE,
94 + REACT_CHUNK_TYPE,
95 } from 'shared/ReactSymbols';
96
97 let hasBadMapPolyfill;
@@ -384,6 +387,11 @@ export function resolveLazyComponentTag(Component: Function): WorkTag {
387 if ($$typeof === REACT_MEMO_TYPE) {
388 return MemoComponent;
389 }
390 + if (enableChunksAPI) {
391 + if ($$typeof === REACT_CHUNK_TYPE) {
392 + return Chunk;
393 + }
394 + }
395 }
396 return IndeterminateComponent;
397 }
@@ -666,6 +674,9 @@ export function createFiberFromTypeAndProps(
674 fiberTag = LazyComponent;
675 resolvedType = null;
676 break getTag;
677 + case REACT_CHUNK_TYPE:
678 + fiberTag = Chunk;
679 + break getTag;
680 case REACT_FUNDAMENTAL_TYPE:
681 if (enableFundamentalAPI) {
682 return createFiberFromFundamental(
packages/react-reconciler/src/ReactFiberBeginWork.js
+106
@@ -42,6 +42,7 @@ import {
42 IncompleteClassComponent,
43 FundamentalComponent,
44 ScopeComponent,
45 + Chunk,
46 } from 'shared/ReactWorkTags';
47 import {
48 NoEffect,
@@ -64,6 +65,7 @@ import {
65 enableFundamentalAPI,
66 warnAboutDefaultPropsOnFunctionComponents,
67 enableScopeAPI,
68 + enableChunksAPI,
69 } from 'shared/ReactFeatureFlags';
70 import invariant from 'shared/invariant';
71 import shallowEqual from 'shared/shallowEqual';
@@ -689,6 +691,82 @@ function updateFunctionComponent(
691 return workInProgress.child;
692 }
693
694 +function updateChunk(
695 + current: Fiber | null,
696 + workInProgress: Fiber,
697 + chunk: any,
698 + nextProps: any,
699 + renderExpirationTime: ExpirationTime,
700 +) {
701 + // TODO: current can be non-null here even if the component
702 + // hasn't yet mounted. This happens after the first render suspends.
703 + // We'll need to figure out if this is fine or can cause issues.
704 +
705 + const render = chunk.render;
706 + const data = chunk.query();
707 +
708 + // The rest is a fork of updateFunctionComponent
709 + let nextChildren;
710 + prepareToReadContext(workInProgress, renderExpirationTime);
711 + if (__DEV__) {
712 + ReactCurrentOwner.current = workInProgress;
713 + setCurrentPhase('render');
714 + nextChildren = renderWithHooks(
715 + current,
716 + workInProgress,
717 + render,
718 + nextProps,
719 + data,
720 + renderExpirationTime,
721 + );
722 + if (
723 + debugRenderPhaseSideEffectsForStrictMode &&
724 + workInProgress.mode & StrictMode
725 + ) {
726 + // Only double-render components with Hooks
727 + if (workInProgress.memoizedState !== null) {
728 + nextChildren = renderWithHooks(
729 + current,
730 + workInProgress,
731 + render,
732 + nextProps,
733 + data,
734 + renderExpirationTime,
735 + );
736 + }
737 + }
738 + setCurrentPhase(null);
739 + } else {
740 + nextChildren = renderWithHooks(
741 + current,
742 + workInProgress,
743 + render,
744 + nextProps,
745 + data,
746 + renderExpirationTime,
747 + );
748 + }
749 +
750 + if (current !== null && !didReceiveUpdate) {
751 + bailoutHooks(current, workInProgress, renderExpirationTime);
752 + return bailoutOnAlreadyFinishedWork(
753 + current,
754 + workInProgress,
755 + renderExpirationTime,
756 + );
757 + }
758 +
759 + // React DevTools reads this flag.
760 + workInProgress.effectTag |= PerformedWork;
761 + reconcileChildren(
762 + current,
763 + workInProgress,
764 + nextChildren,
765 + renderExpirationTime,
766 + );
767 + return workInProgress.child;
768 +}
769 +
770 function updateClassComponent(
771 current: Fiber | null,
772 workInProgress: Fiber,
@@ -1132,6 +1210,20 @@ function mountLazyComponent(
1210 );
1211 return child;
1212 }
1213 + case Chunk: {
1214 + if (enableChunksAPI) {
1215 + // TODO: Resolve for Hot Reloading.
1216 + child = updateChunk(
1217 + null,
1218 + workInProgress,
1219 + Component,
1220 + props,
1221 + renderExpirationTime,
1222 + );
1223 + return child;
1224 + }
1225 + break;
1226 + }
1227 }
1228 let hint = '';
1229 if (__DEV__) {
@@ -3192,6 +3284,20 @@ function beginWork(
3284 }
3285 break;
3286 }
3287 + case Chunk: {
3288 + if (enableChunksAPI) {
3289 + const chunk = workInProgress.type;
3290 + const props = workInProgress.pendingProps;
3291 + return updateChunk(
3292 + current,
3293 + workInProgress,
3294 + chunk,
3295 + props,
3296 + renderExpirationTime,
3297 + );
3298 + }
3299 + break;
3300 + }
3301 }
3302 invariant(
3303 false,
packages/react-reconciler/src/ReactFiberCommitWork.js
+13 -6
@@ -51,6 +51,7 @@ import {
51 SuspenseListComponent,
52 FundamentalComponent,
53 ScopeComponent,
54 + Chunk,
55 } from 'shared/ReactWorkTags';
56 import {
57 invokeGuardedCallback,
@@ -247,7 +248,8 @@ function commitBeforeMutationLifeCycles(
248 switch (finishedWork.tag) {
249 case FunctionComponent:
250 case ForwardRef:
250 - case SimpleMemoComponent: {
251 + case SimpleMemoComponent:
252 + case Chunk: {
253 commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
254 return;
255 }
@@ -396,7 +398,8 @@ export function commitPassiveHookEffects(finishedWork: Fiber): void {
398 switch (finishedWork.tag) {
399 case FunctionComponent:
400 case ForwardRef:
399 - case SimpleMemoComponent: {
401 + case SimpleMemoComponent:
402 + case Chunk: {
403 commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
404 commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
405 break;
@@ -416,7 +419,8 @@ function commitLifeCycles(
419 switch (finishedWork.tag) {
420 case FunctionComponent:
421 case ForwardRef:
419 - case SimpleMemoComponent: {
422 + case SimpleMemoComponent:
423 + case Chunk: {
424 commitHookEffectList(UnmountLayout, MountLayout, finishedWork);
425 return;
426 }
@@ -746,7 +750,8 @@ function commitUnmount(
750 case FunctionComponent:
751 case ForwardRef:
752 case MemoComponent:
749 - case SimpleMemoComponent: {
753 + case SimpleMemoComponent:
754 + case Chunk: {
755 const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
756 if (updateQueue !== null) {
757 const lastEffect = updateQueue.lastEffect;
@@ -1276,7 +1281,8 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1281 case FunctionComponent:
1282 case ForwardRef:
1283 case MemoComponent:
1279 - case SimpleMemoComponent: {
1284 + case SimpleMemoComponent:
1285 + case Chunk: {
1286 // Note: We currently never use MountMutation, but useLayout uses
1287 // UnmountMutation.
1288 commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
@@ -1315,7 +1321,8 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1321 case FunctionComponent:
1322 case ForwardRef:
1323 case MemoComponent:
1318 - case SimpleMemoComponent: {
1324 + case SimpleMemoComponent:
1325 + case Chunk: {
1326 // Note: We currently never use MountMutation, but useLayout uses
1327 // UnmountMutation.
1328 commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
packages/react-reconciler/src/ReactFiberCompleteWork.js
+7
@@ -51,6 +51,7 @@ import {
51 IncompleteClassComponent,
52 FundamentalComponent,
53 ScopeComponent,
54 + Chunk,
55 } from 'shared/ReactWorkTags';
56 import {NoMode, BlockingMode} from './ReactTypeOfMode';
57 import {
@@ -118,6 +119,7 @@ import {
119 enableDeprecatedFlareAPI,
120 enableFundamentalAPI,
121 enableScopeAPI,
122 + enableChunksAPI,
123 } from 'shared/ReactFeatureFlags';
124 import {
125 markSpawnedWork,
@@ -1293,6 +1295,11 @@ function completeWork(
1295 }
1296 break;
1297 }
1298 + case Chunk:
1299 + if (enableChunksAPI) {
1300 + return null;
1301 + }
1302 + break;
1303 }
1304 invariant(
1305 false,
packages/react-reconciler/src/ReactFiberHooks.js
+3 -3
@@ -366,7 +366,7 @@ export function renderWithHooks(
366 workInProgress: Fiber,
367 Component: any,
368 props: any,
369 - refOrContext: any,
369 + secondArg: any,
370 nextRenderExpirationTime: ExpirationTime,
371 ): any {
372 renderExpirationTime = nextRenderExpirationTime;
@@ -422,7 +422,7 @@ export function renderWithHooks(
422 : HooksDispatcherOnUpdate;
423 }
424
425 - let children = Component(props, refOrContext);
425 + let children = Component(props, secondArg);
426
427 if (didScheduleRenderPhaseUpdate) {
428 do {
@@ -449,7 +449,7 @@ export function renderWithHooks(
449 ? HooksDispatcherOnUpdateInDEV
450 : HooksDispatcherOnUpdate;
451
452 - children = Component(props, refOrContext);
452 + children = Component(props, secondArg);
453 } while (didScheduleRenderPhaseUpdate);
454
455 renderPhaseUpdates = null;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+5 -2
@@ -87,6 +87,7 @@ import {
87 ForwardRef,
88 MemoComponent,
89 SimpleMemoComponent,
90 + Chunk,
91 } from 'shared/ReactWorkTags';
92 import {
93 NoEffect,
@@ -2577,7 +2578,8 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
2578 tag !== FunctionComponent &&
2579 tag !== ForwardRef &&
2580 tag !== MemoComponent &&
2580 - tag !== SimpleMemoComponent
2581 + tag !== SimpleMemoComponent &&
2582 + tag !== Chunk
2583 ) {
2584 // Only warn for user-defined components, not internal ones like Suspense.
2585 return;
@@ -2872,7 +2874,8 @@ export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
2874 break;
2875 case FunctionComponent:
2876 case ForwardRef:
2875 - case SimpleMemoComponent: {
2877 + case SimpleMemoComponent:
2878 + case Chunk: {
2879 let firstHook: null | Hook = current.memoizedState;
2880 // TODO: This just checks the first Hook. Isn't it suppose to check all Hooks?
2881 if (firstHook !== null && firstHook.baseQueue !== null) {
packages/react-reconciler/src/__tests__/ReactChunks-test.js new
+200
@@ -0,0 +1,200 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + * @jest-environment node
9 + */
10 +
11 +let React;
12 +let ReactNoop;
13 +let useState;
14 +let Suspense;
15 +let chunk;
16 +let readString;
17 +
18 +describe('ReactChunks', () => {
19 + beforeEach(() => {
20 + jest.resetModules();
21 +
22 + React = require('react');
23 + ReactNoop = require('react-noop-renderer');
24 +
25 + chunk = React.chunk;
26 + useState = React.useState;
27 + Suspense = React.Suspense;
28 + let cache = new Map();
29 + readString = function(text) {
30 + let entry = cache.get(text);
31 + if (!entry) {
32 + entry = {
33 + promise: new Promise(resolve => {
34 + setTimeout(() => {
35 + entry.resolved = true;
36 + resolve();
37 + }, 100);
38 + }),
39 + resolved: false,
40 + };
41 + cache.set(text, entry);
42 + }
43 + if (!entry.resolved) {
44 + throw entry.promise;
45 + }
46 + return text;
47 + };
48 + });
49 +
50 + it.experimental('renders a component with a suspending query', async () => {
51 + function Query(id) {
52 + return {
53 + id: id,
54 + name: readString('Sebastian'),
55 + };
56 + }
57 +
58 + function Render(props, data) {
59 + return (
60 + <span>
61 + {props.title}: {data.name}
62 + </span>
63 + );
64 + }
65 +
66 + let loadUser = chunk(Query, Render);
67 +
68 + function App({User}) {
69 + return (
70 + <Suspense fallback={'Loading...'}>
71 + <User title="Name" />
72 + </Suspense>
73 + );
74 + }
75 +
76 + await ReactNoop.act(async () => {
77 + ReactNoop.render(<App User={loadUser(123)} />);
78 + });
79 +
80 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
81 +
82 + await ReactNoop.act(async () => {
83 + jest.advanceTimersByTime(1000);
84 + });
85 +
86 + expect(ReactNoop).toMatchRenderedOutput(<span>Name: Sebastian</span>);
87 + });
88 +
89 + it.experimental('supports a lazy wrapper around a chunk', async () => {
90 + function Query(id) {
91 + return {
92 + id: id,
93 + name: readString('Sebastian'),
94 + };
95 + }
96 +
97 + function Render(props, data) {
98 + return (
99 + <span>
100 + {props.title}: {data.name}
101 + </span>
102 + );
103 + }
104 +
105 + let loadUser = chunk(Query, Render);
106 +
107 + function App({User}) {
108 + return (
109 + <Suspense fallback={'Loading...'}>
110 + <User title="Name" />
111 + </Suspense>
112 + );
113 + }
114 +
115 + let resolveLazy;
116 + let LazyUser = React.lazy(
117 + () =>
118 + new Promise(resolve => {
119 + resolveLazy = function() {
120 + resolve({
121 + default: loadUser(123),
122 + });
123 + };
124 + }),
125 + );
126 +
127 + await ReactNoop.act(async () => {
128 + ReactNoop.render(<App User={LazyUser} />);
129 + });
130 +
131 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
132 +
133 + // Resolve the component.
134 + await ReactNoop.act(async () => {
135 + await resolveLazy();
136 + });
137 +
138 + // We're still waiting on the data.
139 + expect(ReactNoop).toMatchRenderedOutput('Loading...');
140 +
141 + await ReactNoop.act(async () => {
142 + jest.advanceTimersByTime(1000);
143 + });
144 +
145 + expect(ReactNoop).toMatchRenderedOutput(<span>Name: Sebastian</span>);
146 + });
147 +
148 + it.experimental(
149 + 'can receive updated data for the same component',
150 + async () => {
151 + function Query(firstName) {
152 + return {
153 + name: firstName,
154 + };
155 + }
156 +
157 + function Render(props, data) {
158 + let [initialName] = useState(data.name);
159 + return (
160 + <>
161 + <span>Initial name: {initialName}</span>
162 + <span>Latest name: {data.name}</span>
163 + </>
164 + );
165 + }
166 +
167 + let loadUser = chunk(Query, Render);
168 +
169 + function App({User}) {
170 + return (
171 + <Suspense fallback={'Loading...'}>
172 + <User title="Name" />
173 + </Suspense>
174 + );
175 + }
176 +
177 + await ReactNoop.act(async () => {
178 + ReactNoop.render(<App User={loadUser('Sebastian')} />);
179 + });
180 +
181 + expect(ReactNoop).toMatchRenderedOutput(
182 + <>
183 + <span>Initial name: Sebastian</span>
184 + <span>Latest name: Sebastian</span>
185 + </>,
186 + );
187 +
188 + await ReactNoop.act(async () => {
189 + ReactNoop.render(<App User={loadUser('Dan')} />);
190 + });
191 +
192 + expect(ReactNoop).toMatchRenderedOutput(
193 + <>
194 + <span>Initial name: Sebastian</span>
195 + <span>Latest name: Dan</span>
196 + </>,
197 + );
198 + },
199 + );
200 +});
packages/react-test-renderer/src/ReactTestRenderer.js
+10
@@ -36,6 +36,7 @@ import {
36 Profiler,
37 MemoComponent,
38 SimpleMemoComponent,
39 + Chunk,
40 IncompleteClassComponent,
41 ScopeComponent,
42 } from 'shared/ReactWorkTags';
@@ -185,6 +186,14 @@ function toTree(node: ?Fiber) {
186 instance: null,
187 rendered: childrenToTree(node.child),
188 };
189 + case Chunk:
190 + return {
191 + nodeType: 'chunk',
192 + type: node.type,
193 + props: {...node.memoizedProps},
194 + instance: null,
195 + rendered: childrenToTree(node.child),
196 + };
197 case HostComponent: {
198 return {
199 nodeType: 'host',
@@ -222,6 +231,7 @@ const validWrapperTypes = new Set([
231 ForwardRef,
232 MemoComponent,
233 SimpleMemoComponent,
234 + Chunk,
235 // Normally skipped, but used when there's more than one root child.
236 HostRoot,
237 ]);
packages/react/src/React.js
+6
@@ -28,6 +28,7 @@ import {createContext} from './ReactContext';
28 import {lazy} from './ReactLazy';
29 import forwardRef from './forwardRef';
30 import memo from './memo';
31 +import chunk from './chunk';
32 import {
33 useCallback,
34 useContext,
@@ -62,6 +63,7 @@ import {
63 enableFundamentalAPI,
64 enableScopeAPI,
65 exposeConcurrentModeAPIs,
66 + enableChunksAPI,
67 } from 'shared/ReactFeatureFlags';
68 const React = {
69 Children: {
@@ -114,6 +116,10 @@ if (exposeConcurrentModeAPIs) {
116 React.unstable_withSuspenseConfig = withSuspenseConfig;
117 }
118
119 +if (enableChunksAPI) {
120 + React.chunk = chunk;
121 +}
122 +
123 if (enableDeprecatedFlareAPI) {
124 React.DEPRECATED_useResponder = useResponder;
125 React.DEPRECATED_createResponder = createResponder;
packages/react/src/chunk.js new
+76
@@ -0,0 +1,76 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import {
9 + REACT_CHUNK_TYPE,
10 + REACT_MEMO_TYPE,
11 + REACT_FORWARD_REF_TYPE,
12 +} from 'shared/ReactSymbols';
13 +
14 +opaque type Chunk<Props>: React$AbstractComponent<
15 + Props,
16 + null,
17 +> = React$AbstractComponent<Props, null>;
18 +
19 +export default function chunk<Args, Props, Data>(
20 + query: (...args: Args) => Data,
21 + render: (props: Props, data: Data) => React$Node,
22 +): (...args: Args) => Chunk<Props> {
23 + if (__DEV__) {
24 + if (typeof query !== 'function') {
25 + console.error(
26 + 'Chunks require a query function but was given %s.',
27 + query === null ? 'null' : typeof query,
28 + );
29 + }
30 + if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
31 + console.error(
32 + 'Chunks require a render function but received a `memo` ' +
33 + 'component. Use `memo` on an inner component instead.',
34 + );
35 + } else if (render != null && render.$$typeof === REACT_FORWARD_REF_TYPE) {
36 + console.error(
37 + 'Chunks require a render function but received a `forwardRef` ' +
38 + 'component. Use `forwardRef` on an inner component instead.',
39 + );
40 + } else if (typeof render !== 'function') {
41 + console.error(
42 + 'Chunks require a render function but was given %s.',
43 + render === null ? 'null' : typeof render,
44 + );
45 + } else if (render.length !== 0 && render.length !== 2) {
46 + // Warn if it's not accepting two args.
47 + // Do not warn for 0 arguments because it could be due to usage of the 'arguments' object
48 + console.error(
49 + 'Chunk render functions accept exactly two parameters: props and data. %s',
50 + render.length === 1
51 + ? 'Did you forget to use the data parameter?'
52 + : 'Any additional parameter will be undefined.',
53 + );
54 + }
55 +
56 + if (
57 + render != null &&
58 + (render.defaultProps != null || render.propTypes != null)
59 + ) {
60 + console.error(
61 + 'Chunk render functions do not support propTypes or defaultProps. ' +
62 + 'Did you accidentally pass a React component?',
63 + );
64 + }
65 + }
66 + return function(): Chunk<Props> {
67 + let args = arguments;
68 + return {
69 + $$typeof: REACT_CHUNK_TYPE,
70 + query: function() {
71 + return query.apply(null, args);
72 + },
73 + render: render,
74 + };
75 + };
76 +}
packages/shared/ReactFeatureFlags.js
+3
@@ -30,6 +30,9 @@ export const enableSchedulerTracing = __PROFILE__;
30 export const enableSuspenseServerRenderer = __EXPERIMENTAL__;
31 export const enableSelectiveHydration = __EXPERIMENTAL__;
32
33 +// Flight experiments
34 +export const enableChunksAPI = __EXPERIMENTAL__;
35 +
36 // Only used in www builds.
37 export const enableSchedulerDebugging = false;
38
packages/shared/ReactSymbols.js
+1
@@ -51,6 +51,7 @@ export const REACT_SUSPENSE_LIST_TYPE = hasSymbol
51 : 0xead8;
52 export const REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3;
53 export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4;
54 +export const REACT_CHUNK_TYPE = hasSymbol ? Symbol.for('react.chunk') : 0xead9;
55 export const REACT_FUNDAMENTAL_TYPE = hasSymbol
56 ? Symbol.for('react.fundamental')
57 : 0xead5;
packages/shared/ReactWorkTags.js
+3 -1
@@ -29,7 +29,8 @@ export type WorkTag =
29 | 18
30 | 19
31 | 20
32 - | 21;
32 + | 21
33 + | 22;
34
35 export const FunctionComponent = 0;
36 export const ClassComponent = 1;
@@ -53,3 +54,4 @@ export const DehydratedFragment = 18;
54 export const SuspenseListComponent = 19;
55 export const FundamentalComponent = 20;
56 export const ScopeComponent = 21;
57 +export const Chunk = 22;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -23,6 +23,7 @@ export const enableProfilerTimer = __PROFILE__;
23 export const enableSchedulerTracing = __PROFILE__;
24 export const enableSuspenseServerRenderer = false;
25 export const enableSelectiveHydration = false;
26 +export const enableChunksAPI = false;
27 export const exposeConcurrentModeAPIs = __EXPERIMENTAL__;
28 export const warnAboutShorthandPropertyCollision = false;
29 export const enableSchedulerDebugging = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerTimer = __PROFILE__;
20 export const enableSchedulerTracing = __PROFILE__;
21 export const enableSuspenseServerRenderer = false;
22 export const enableSelectiveHydration = false;
23 +export const enableChunksAPI = false;
24 export const disableJavaScriptURLs = false;
25 export const disableInputAttributeSyncing = false;
26 export const exposeConcurrentModeAPIs = __EXPERIMENTAL__;
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerTimer = __PROFILE__;
20 export const enableSchedulerTracing = __PROFILE__;
21 export const enableSuspenseServerRenderer = false;
22 export const enableSelectiveHydration = false;
23 +export const enableChunksAPI = false;
24 export const disableJavaScriptURLs = false;
25 export const disableInputAttributeSyncing = false;
26 export const exposeConcurrentModeAPIs = __EXPERIMENTAL__;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerTimer = __PROFILE__;
20 export const enableSchedulerTracing = __PROFILE__;
21 export const enableSuspenseServerRenderer = false;
22 export const enableSelectiveHydration = false;
23 +export const enableChunksAPI = false;
24 export const disableJavaScriptURLs = false;
25 export const disableInputAttributeSyncing = false;
26 export const exposeConcurrentModeAPIs = __EXPERIMENTAL__;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -20,6 +20,7 @@ export const enableProfilerTimer = __PROFILE__;
20 export const enableSchedulerTracing = __PROFILE__;
21 export const enableSuspenseServerRenderer = false;
22 export const enableSelectiveHydration = false;
23 +export const enableChunksAPI = false;
24 export const exposeConcurrentModeAPIs = __EXPERIMENTAL__;
25 export const enableSchedulerDebugging = false;
26 export const disableJavaScriptURLs = false;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -43,6 +43,8 @@ export const exposeConcurrentModeAPIs = __EXPERIMENTAL__;
43
44 export const enableSuspenseServerRenderer = true;
45
46 +export const enableChunksAPI = __EXPERIMENTAL__;
47 +
48 export const disableJavaScriptURLs = true;
49
50 let refCount = 0;
packages/shared/getComponentName.js
+3
@@ -21,6 +21,7 @@ import {
21 REACT_SUSPENSE_TYPE,
22 REACT_SUSPENSE_LIST_TYPE,
23 REACT_LAZY_TYPE,
24 + REACT_CHUNK_TYPE,
25 } from 'shared/ReactSymbols';
26 import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
27
@@ -79,6 +80,8 @@ function getComponentName(type: mixed): string | null {
80 return getWrappedName(type, type.render, 'ForwardRef');
81 case REACT_MEMO_TYPE:
82 return getComponentName(type.type);
83 + case REACT_CHUNK_TYPE:
84 + return getComponentName(type.render);
85 case REACT_LAZY_TYPE: {
86 const thenable: LazyComponent<mixed> = (type: any);
87 const resolvedThenable = refineResolvedLazyComponent(thenable);
packages/shared/isValidElementType.js
+3 -1
@@ -22,6 +22,7 @@ import {
22 REACT_FUNDAMENTAL_TYPE,
23 REACT_RESPONDER_TYPE,
24 REACT_SCOPE_TYPE,
25 + REACT_CHUNK_TYPE,
26 } from 'shared/ReactSymbols';
27
28 export default function isValidElementType(type: mixed) {
@@ -44,6 +45,7 @@ export default function isValidElementType(type: mixed) {
45 type.$$typeof === REACT_FORWARD_REF_TYPE ||
46 type.$$typeof === REACT_FUNDAMENTAL_TYPE ||
47 type.$$typeof === REACT_RESPONDER_TYPE ||
47 - type.$$typeof === REACT_SCOPE_TYPE))
48 + type.$$typeof === REACT_SCOPE_TYPE ||
49 + type.$$typeof === REACT_CHUNK_TYPE))
50 );
51 }