[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
}