@samitouri / QOS-React-2 / commits / b52a5624e9

[CS] Persistent Updates (#11260)

* Update build size * [CS] Clone container instead of new root concept The extra "root" concept is kind of unnecessary. Instead of having a mutable container even in the persistent mode, I'll instead make the container be immutable too and be cloned. Then the "commit" just becomes swapping the previous container for the new one. * Change the signature or persistence again We may need to clone without any updates, e.g. when the children are changed. Passing in the previous node is not enough to recycle since it won't have the up-to-date props and children. It's really only useful to for allocation pooling. * Implement persistent updates This forks the update path for host fibers. For mutation mode we mark them as having an effect. For persistence mode, we clone the stateNode with new props/children. Next I'll do HostRoot and HostPortal. * Refine protocol into a complete and commit phase finalizeContainerChildren will get called at the complete phase. replaceContainer will get called at commit. Also, drop the keepChildren flag. We'll never keep children as we'll never update a container if none of the children has changed. * Implement persistent updates of roots and portals These are both "containers". Normally we rely on placement/deletion effects to deal with insertions into the containers. In the persistent mode we need to clone the container and append all the changed children to it. I needed somewhere to store these new containers before they're committed so I added another field. * Commit persistent work at the end by swapping out the container * Unify cloneOrRecycle Originally I tried to make the recyclable instance nullable but Flow didn't like that and it's kind of sketchy since the instance type might not be nullable. However, the real difference which one we call is depending on whether they are equal. We can just offload that to the renderer. Most of them won't need to know about this at all since they'll always clone or just create new. The ones that do know now have to be careful to compare them so they don't reuse an existing instance but that's probably fine to simplify the implementation and API. * Add persistent noop renderer for testing * Add basic persistent tree test * Test bail out This adds a test for bailouts. This revealed a subtle bug. We don't set the return pointer when stepping into newly created fibers because there can only be one. However, since I'm reusing this mechanism for persistent updates, I'll need to set the return pointer because a bailed out tree won't have the right return pointer. * Test persistent text nodes Found another bug. * Add persistent portal test This creates a bit of an unfortunate feature testing in the unmount branch. That's because we want to trigger nested host deletions in portals in the mutation mode. * Don't consider container when determining portal identity Basically, we can't use the container to determine if we should keep identity and update an existing portal instead of recreate it. Because for persistent containers, there is no permanent identity. This makes it kind of strange to even use portals in this mode. It's probably more ideal to have another concept that has permanent identity rather than trying to swap out containers. * Clear portals when the portal is deleted When a portal gets deleted we need to create a new empty container and replace the current one with the empty one. * Add renderer mode flags for dead code elimination * Simplify ReactNoop fix * Add new type to the host config for persistent configs We need container to stay as the persistent identity of the root atom. So that we can refer to portals over time. Instead, I'll introduce a new type just to temporarily hold the children of a container until they're ready to be committed into the permanent container. Essentially, this is just a fancy array that is not an array so that the host can choose data structure/allocation for it. * Implement new hooks Now containers are singletons and instead their children swap. That way portals can use the container as part of their identity again. * Update build size and error codes * Address comment * Move new files to new location

Sebastian Markbåge committed Oct 18, 2017 at 18:28 UTC b52a5624e95f77166ffa520476d68231640692f9
18 files changed +757 -135
packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js new
+24
@@ -0,0 +1,24 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
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 + * @providesModule ReactNativeCSFeatureFlags
8 + * @flow
9 + */
10 +
11 +'use strict';
12 +
13 +import type {FeatureFlags} from 'ReactFeatureFlags';
14 +
15 +var ReactNativeCSFeatureFlags: FeatureFlags = {
16 + enableAsyncSubtreeAPI: true,
17 + enableAsyncSchedulingByDefaultInReactDOM: false,
18 + // React Native CS uses persistent reconciler.
19 + enableMutatingReconciler: false,
20 + enableNoopReconciler: false,
21 + enablePersistentReconciler: true,
22 +};
23 +
24 +module.exports = ReactNativeCSFeatureFlags;
packages/react-cs-renderer/src/ReactNativeCSFiberEntry.js
+21 -18
@@ -154,34 +154,37 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
154 persistence: {
155 cloneInstance(
156 instance: Instance,
157 - updatePayload: Object,
158 - type: string,
159 - oldProps: Props,
160 - newProps: Props,
161 - internalInstanceHandle: Object,
162 - keepChildren: boolean,
163 - ): Instance {
164 - return 0;
165 - },
166 - tryToReuseInstance(
167 - instance: Instance,
168 - updatePayload: Object,
157 + updatePayload: null | Object,
158 type: string,
159 oldProps: Props,
160 newProps: Props,
161 internalInstanceHandle: Object,
162 keepChildren: boolean,
163 + recyclableInstance: null | Instance,
164 ): Instance {
165 return 0;
166 },
167
178 - createRootInstance(
179 - rootContainerInstance: Container,
180 - hostContext: {},
181 - ): Instance {
182 - return 123;
168 + createContainerChildSet(
169 + container: Container,
170 + ): Array<Instance | TextInstance> {
171 + return [];
172 },
184 - commitRootInstance(rootInstance: Instance): void {},
173 +
174 + appendChildToContainerChildSet(
175 + childSet: Array<Instance | TextInstance>,
176 + child: Instance | TextInstance,
177 + ): void {},
178 +
179 + finalizeContainerChildren(
180 + container: Container,
181 + newChildren: Array<Instance | TextInstance>,
182 + ): void {},
183 +
184 + replaceContainerChildren(
185 + container: Container,
186 + newChildren: Array<Instance | TextInstance>,
187 + ): void {},
188 },
189 });
190
packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.js
+2
@@ -12,6 +12,8 @@
12 var React;
13 var ReactNativeCS;
14
15 +jest.mock('ReactFeatureFlags', () => require('ReactNativeCSFeatureFlags'));
16 +
17 describe('ReactNativeCS', () => {
18 beforeEach(() => {
19 jest.resetModules();
packages/react-noop-renderer/src/ReactNoopEntry.js
+81 -1
@@ -20,6 +20,7 @@
20 import type {Fiber} from 'ReactFiber';
21 import type {UpdateQueue} from 'ReactFiberUpdateQueue';
22
23 +var ReactFeatureFlags = require('ReactFeatureFlags');
24 var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
25 var ReactFiberReconciler = require('ReactFiberReconciler');
26 var ReactInstanceMap = require('ReactInstanceMap');
@@ -85,7 +86,7 @@ function removeChild(
86
87 let elapsedTimeInMs = 0;
88
88 -var NoopRenderer = ReactFiberReconciler({
89 +var SharedHostConfig = {
90 getRootHostContext() {
91 if (failInBeginPhase) {
92 throw new Error('Error in host config.');
@@ -176,7 +177,10 @@ var NoopRenderer = ReactFiberReconciler({
177 now(): number {
178 return elapsedTimeInMs;
179 },
180 +};
181
182 +var NoopRenderer = ReactFiberReconciler({
183 + ...SharedHostConfig,
184 mutation: {
185 commitMount(instance: Instance, type: string, newProps: Props): void {
186 // Noop
@@ -211,8 +215,64 @@ var NoopRenderer = ReactFiberReconciler({
215 },
216 });
217
218 +var PersistentNoopRenderer = ReactFeatureFlags.enablePersistentReconciler
219 + ? ReactFiberReconciler({
220 + ...SharedHostConfig,
221 + persistence: {
222 + cloneInstance(
223 + instance: Instance,
224 + updatePayload: null | Object,
225 + type: string,
226 + oldProps: Props,
227 + newProps: Props,
228 + internalInstanceHandle: Object,
229 + keepChildren: boolean,
230 + recyclableInstance: null | Instance,
231 + ): Instance {
232 + const clone = {
233 + id: instance.id,
234 + type: type,
235 + children: keepChildren ? instance.children : [],
236 + prop: newProps.prop,
237 + };
238 + Object.defineProperty(clone, 'id', {
239 + value: clone.id,
240 + enumerable: false,
241 + });
242 + return clone;
243 + },
244 +
245 + createContainerChildSet(
246 + container: Container,
247 + ): Array<Instance | TextInstance> {
248 + return [];
249 + },
250 +
251 + appendChildToContainerChildSet(
252 + childSet: Array<Instance | TextInstance>,
253 + child: Instance | TextInstance,
254 + ): void {
255 + childSet.push(child);
256 + },
257 +
258 + finalizeContainerChildren(
259 + container: Container,
260 + newChildren: Array<Instance | TextInstance>,
261 + ): void {},
262 +
263 + replaceContainerChildren(
264 + container: Container,
265 + newChildren: Array<Instance | TextInstance>,
266 + ): void {
267 + container.children = newChildren;
268 + },
269 + },
270 + })
271 + : null;
272 +
273 var rootContainers = new Map();
274 var roots = new Map();
275 +var persistentRoots = new Map();
276 var DEFAULT_ROOT_ID = '<default>';
277
278 let yieldedValues = null;
@@ -275,6 +335,26 @@ var ReactNoop = {
335 NoopRenderer.updateContainer(element, root, null, callback);
336 },
337
338 + renderToPersistentRootWithID(
339 + element: React$Element<any>,
340 + rootID: string,
341 + callback: ?Function,
342 + ) {
343 + if (PersistentNoopRenderer === null) {
344 + throw new Error(
345 + 'Enable ReactFeatureFlags.enablePersistentReconciler to use it in tests.',
346 + );
347 + }
348 + let root = persistentRoots.get(rootID);
349 + if (!root) {
350 + const container = {rootID: rootID, children: []};
351 + rootContainers.set(rootID, container);
352 + root = PersistentNoopRenderer.createContainer(container, false);
353 + persistentRoots.set(rootID, root);
354 + }
355 + PersistentNoopRenderer.updateContainer(element, root, null, callback);
356 + },
357 +
358 unmountRootWithID(rootID: string) {
359 const root = roots.get(rootID);
360 if (root) {
packages/react-reconciler/src/ReactFiber.js
+1
@@ -445,6 +445,7 @@ exports.createFiberFromPortal = function(
445 fiber.expirationTime = expirationTime;
446 fiber.stateNode = {
447 containerInfo: portal.containerInfo,
448 + pendingChildren: null, // Used by persistent updates
449 implementation: portal.implementation,
450 };
451 return fiber;
packages/react-reconciler/src/ReactFiberBeginWork.js
+2 -2
@@ -67,8 +67,8 @@ if (__DEV__) {
67 var warnedAboutStatelessRefs = {};
68 }
69
70 -module.exports = function<T, P, I, TI, PI, C, CX, PL>(
71 - config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
70 +module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
71 + config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
72 hostContext: HostContext<C, CX>,
73 hydrationContext: HydrationContext<C, CX>,
74 scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
packages/react-reconciler/src/ReactFiberCommitWork.js
+104 -30
@@ -13,6 +13,7 @@
13 import type {Fiber} from 'ReactFiber';
14 import type {HostConfig} from 'ReactFiberReconciler';
15
16 +var ReactFeatureFlags = require('ReactFeatureFlags');
17 var ReactTypeOfWork = require('ReactTypeOfWork');
18 var {
19 ClassComponent,
@@ -38,11 +39,11 @@ if (__DEV__) {
39 var {startPhaseTimer, stopPhaseTimer} = require('ReactDebugFiberPerf');
40 }
41
41 -module.exports = function<T, P, I, TI, PI, C, CX, PL>(
42 - config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
42 +module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
43 + config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
44 captureError: (failedFiber: Fiber, error: mixed) => Fiber | null,
45 ) {
45 - const {getPublicInstance} = config;
46 + const {getPublicInstance, mutation, persistence} = config;
47
48 if (__DEV__) {
49 var callComponentWillUnmountWithTimerInDev = function(current, instance) {
@@ -162,6 +163,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
163 // We have no life-cycles associated with text.
164 return;
165 }
166 + case HostPortal: {
167 + // We have no life-cycles associated with portals.
168 + return;
169 + }
170 default: {
171 invariant(
172 false,
@@ -222,7 +227,14 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
227 // TODO: this is recursive.
228 // We are also not using this parent because
229 // the portal will get pushed immediately.
225 - unmountHostComponents(current);
230 + if (ReactFeatureFlags.enableMutatingReconciler && mutation) {
231 + unmountHostComponents(current);
232 + } else if (
233 + ReactFeatureFlags.enablePersistentReconciler &&
234 + persistence
235 + ) {
236 + emptyPortalContainer(current);
237 + }
238 return;
239 }
240 }
@@ -239,7 +251,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
251 commitUnmount(node);
252 // Visit children because they may contain more composite or host nodes.
253 // Skip portals because commitUnmount() currently visits them recursively.
242 - if (node.child !== null && node.tag !== HostPortal) {
254 + if (
255 + node.child !== null &&
256 + // If we use mutation we drill down into portals using commitUnmount above.
257 + // If we don't use mutation we drill down into portals here instead.
258 + (!mutation || node.tag !== HostPortal)
259 + ) {
260 node.child.return = node;
261 node = node.child;
262 continue;
@@ -272,22 +289,75 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
289 }
290 }
291
275 - if (!config.mutation) {
276 - return {
277 - commitResetTextContent(finishedWork: Fiber) {},
278 - commitPlacement(finishedWork: Fiber) {},
279 - commitDeletion(current: Fiber) {
280 - // Detach refs and call componentWillUnmount() on the whole subtree.
281 - commitNestedUnmounts(current);
282 - detachFiber(current);
283 - },
284 - commitWork(current: Fiber | null, finishedWork: Fiber) {},
285 - commitLifeCycles,
286 - commitAttachRef,
287 - commitDetachRef,
288 - };
292 + if (!mutation) {
293 + let commitContainer;
294 + if (persistence) {
295 + const {replaceContainerChildren, createContainerChildSet} = persistence;
296 + var emptyPortalContainer = function(current: Fiber) {
297 + const portal: {containerInfo: C, pendingChildren: CC} =
298 + current.stateNode;
299 + const {containerInfo} = portal;
300 + const emptyChildSet = createContainerChildSet(containerInfo);
301 + replaceContainerChildren(containerInfo, emptyChildSet);
302 + };
303 + commitContainer = function(finishedWork: Fiber) {
304 + switch (finishedWork.tag) {
305 + case ClassComponent: {
306 + return;
307 + }
308 + case HostComponent: {
309 + return;
310 + }
311 + case HostText: {
312 + return;
313 + }
314 + case HostRoot:
315 + case HostPortal: {
316 + const portalOrRoot: {containerInfo: C, pendingChildren: CC} =
317 + finishedWork.stateNode;
318 + const {containerInfo, pendingChildren} = portalOrRoot;
319 + replaceContainerChildren(containerInfo, pendingChildren);
320 + return;
321 + }
322 + default: {
323 + invariant(
324 + false,
325 + 'This unit of work tag should not have side-effects. This error is ' +
326 + 'likely caused by a bug in React. Please file an issue.',
327 + );
328 + }
329 + }
330 + };
331 + } else {
332 + commitContainer = function(finishedWork: Fiber) {
333 + // Noop
334 + };
335 + }
336 + if (
337 + ReactFeatureFlags.enablePersistentReconciler ||
338 + ReactFeatureFlags.enableNoopReconciler
339 + ) {
340 + return {
341 + commitResetTextContent(finishedWork: Fiber) {},
342 + commitPlacement(finishedWork: Fiber) {},
343 + commitDeletion(current: Fiber) {
344 + // Detach refs and call componentWillUnmount() on the whole subtree.
345 + commitNestedUnmounts(current);
346 + detachFiber(current);
347 + },
348 + commitWork(current: Fiber | null, finishedWork: Fiber) {
349 + commitContainer(finishedWork);
350 + },
351 + commitLifeCycles,
352 + commitAttachRef,
353 + commitDetachRef,
354 + };
355 + } else if (persistence) {
356 + invariant(false, 'Persistent reconciler is disabled.');
357 + } else {
358 + invariant(false, 'Noop reconciler is disabled.');
359 + }
360 }
290 -
361 const {
362 commitMount,
363 commitUpdate,
@@ -299,7 +369,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
369 insertInContainerBefore,
370 removeChild,
371 removeChildFromContainer,
302 - } = config.mutation;
372 + } = mutation;
373
374 function getHostParentFiber(fiber: Fiber): Fiber {
375 let parent = fiber.return;
@@ -599,13 +669,17 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
669 resetTextContent(current.stateNode);
670 }
671
602 - return {
603 - commitResetTextContent,
604 - commitPlacement,
605 - commitDeletion,
606 - commitWork,
607 - commitLifeCycles,
608 - commitAttachRef,
609 - commitDetachRef,
610 - };
672 + if (ReactFeatureFlags.enableMutatingReconciler) {
673 + return {
674 + commitResetTextContent,
675 + commitPlacement,
676 + commitDeletion,
677 + commitWork,
678 + commitLifeCycles,
679 + commitAttachRef,
680 + commitDetachRef,
681 + };
682 + } else {
683 + invariant(false, 'Mutating reconciler is disabled.');
684 + }
685 };
packages/react-reconciler/src/ReactFiberCompleteWork.js
+227 -12
@@ -23,6 +23,7 @@ var {
23 popContextProvider,
24 popTopLevelContextObject,
25 } = require('ReactFiberContext');
26 +var ReactFeatureFlags = require('ReactFeatureFlags');
27 var ReactTypeOfWork = require('ReactTypeOfWork');
28 var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
29 var ReactFiberExpirationTime = require('ReactFiberExpirationTime');
@@ -44,8 +45,8 @@ var {Never} = ReactFiberExpirationTime;
45
46 var invariant = require('fbjs/lib/invariant');
47
47 -module.exports = function<T, P, I, TI, PI, C, CX, PL>(
48 - config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
48 +module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
49 + config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
50 hostContext: HostContext<C, CX>,
51 hydrationContext: HydrationContext<C, CX>,
52 ) {
@@ -55,6 +56,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
56 appendInitialChild,
57 finalizeInitialChildren,
58 prepareUpdate,
59 + mutation,
60 + persistence,
61 } = config;
62
63 const {
@@ -161,6 +164,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
164 // down its children. Instead, we'll get insertions from each child in
165 // the portal directly.
166 } else if (node.child !== null) {
167 + node.child.return = node;
168 node = node.child;
169 continue;
170 }
@@ -173,10 +177,218 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
177 }
178 node = node.return;
179 }
180 + node.sibling.return = node.return;
181 node = node.sibling;
182 }
183 }
184
185 + let updateHostContainer;
186 + let updateHostComponent;
187 + let updateHostText;
188 + if (mutation) {
189 + if (ReactFeatureFlags.enableMutatingReconciler) {
190 + // Mutation mode
191 + updateHostContainer = function(workInProgress: Fiber) {
192 + // Noop
193 + };
194 + updateHostComponent = function(
195 + current: Fiber,
196 + workInProgress: Fiber,
197 + updatePayload: null | PL,
198 + type: T,
199 + oldProps: P,
200 + newProps: P,
201 + rootContainerInstance: C,
202 + ) {
203 + // TODO: Type this specific to this type of component.
204 + workInProgress.updateQueue = (updatePayload: any);
205 + // If the update payload indicates that there is a change or if there
206 + // is a new ref we mark this as an update. All the work is done in commitWork.
207 + if (updatePayload) {
208 + markUpdate(workInProgress);
209 + }
210 + };
211 + updateHostText = function(
212 + current: Fiber,
213 + workInProgress: Fiber,
214 + oldText: string,
215 + newText: string,
216 + ) {
217 + // If the text differs, mark it as an update. All the work in done in commitWork.
218 + if (oldText !== newText) {
219 + markUpdate(workInProgress);
220 + }
221 + };
222 + } else {
223 + invariant(false, 'Mutating reconciler is disabled.');
224 + }
225 + } else if (persistence) {
226 + if (ReactFeatureFlags.enablePersistentReconciler) {
227 + // Persistent host tree mode
228 + const {
229 + cloneInstance,
230 + createContainerChildSet,
231 + appendChildToContainerChildSet,
232 + finalizeContainerChildren,
233 + } = persistence;
234 +
235 + // An unfortunate fork of appendAllChildren because we have two different parent types.
236 + const appendAllChildrenToContainer = function(
237 + containerChildSet: CC,
238 + workInProgress: Fiber,
239 + ) {
240 + // We only have the top Fiber that was created but we need recurse down its
241 + // children to find all the terminal nodes.
242 + let node = workInProgress.child;
243 + while (node !== null) {
244 + if (node.tag === HostComponent || node.tag === HostText) {
245 + appendChildToContainerChildSet(containerChildSet, node.stateNode);
246 + } else if (node.tag === HostPortal) {
247 + // If we have a portal child, then we don't want to traverse
248 + // down its children. Instead, we'll get insertions from each child in
249 + // the portal directly.
250 + } else if (node.child !== null) {
251 + node.child.return = node;
252 + node = node.child;
253 + continue;
254 + }
255 + if (node === workInProgress) {
256 + return;
257 + }
258 + while (node.sibling === null) {
259 + if (node.return === null || node.return === workInProgress) {
260 + return;
261 + }
262 + node = node.return;
263 + }
264 + node.sibling.return = node.return;
265 + node = node.sibling;
266 + }
267 + };
268 + updateHostContainer = function(workInProgress: Fiber) {
269 + const portalOrRoot: {containerInfo: C, pendingChildren: CC} =
270 + workInProgress.stateNode;
271 + const childrenUnchanged = workInProgress.firstEffect === null;
272 + if (childrenUnchanged) {
273 + // No changes, just reuse the existing instance.
274 + } else {
275 + const container = portalOrRoot.containerInfo;
276 + let newChildSet = createContainerChildSet(container);
277 + if (finalizeContainerChildren(container, newChildSet)) {
278 + markUpdate(workInProgress);
279 + }
280 + portalOrRoot.pendingChildren = newChildSet;
281 + // If children might have changed, we have to add them all to the set.
282 + appendAllChildrenToContainer(newChildSet, workInProgress);
283 + // Schedule an update on the container to swap out the container.
284 + markUpdate(workInProgress);
285 + }
286 + };
287 + updateHostComponent = function(
288 + current: Fiber,
289 + workInProgress: Fiber,
290 + updatePayload: null | PL,
291 + type: T,
292 + oldProps: P,
293 + newProps: P,
294 + rootContainerInstance: C,
295 + ) {
296 + // If there are no effects associated with this node, then none of our children had any updates.
297 + // This guarantees that we can reuse all of them.
298 + const childrenUnchanged = workInProgress.firstEffect === null;
299 + const currentInstance = current.stateNode;
300 + if (childrenUnchanged && updatePayload === null) {
301 + // No changes, just reuse the existing instance.
302 + // Note that this might release a previous clone.
303 + workInProgress.stateNode = currentInstance;
304 + } else {
305 + let recyclableInstance = workInProgress.stateNode;
306 + let newInstance = cloneInstance(
307 + currentInstance,
308 + updatePayload,
309 + type,
310 + oldProps,
311 + newProps,
312 + workInProgress,
313 + childrenUnchanged,
314 + recyclableInstance,
315 + );
316 + if (
317 + finalizeInitialChildren(
318 + newInstance,
319 + type,
320 + newProps,
321 + rootContainerInstance,
322 + )
323 + ) {
324 + markUpdate(workInProgress);
325 + }
326 + workInProgress.stateNode = newInstance;
327 + if (childrenUnchanged) {
328 + // If there are no other effects in this tree, we need to flag this node as having one.
329 + // Even though we're not going to use it for anything.
330 + // Otherwise parents won't know that there are new children to propagate upwards.
331 + markUpdate(workInProgress);
332 + } else {
333 + // If children might have changed, we have to add them all to the set.
334 + appendAllChildren(newInstance, workInProgress);
335 + }
336 + }
337 + };
338 + updateHostText = function(
339 + current: Fiber,
340 + workInProgress: Fiber,
341 + oldText: string,
342 + newText: string,
343 + ) {
344 + if (oldText !== newText) {
345 + // If the text content differs, we'll create a new text instance for it.
346 + const rootContainerInstance = getRootHostContainer();
347 + const currentHostContext = getHostContext();
348 + workInProgress.stateNode = createTextInstance(
349 + newText,
350 + rootContainerInstance,
351 + currentHostContext,
352 + workInProgress,
353 + );
354 + // We'll have to mark it as having an effect, even though we won't use the effect for anything.
355 + // This lets the parents know that at least one of their children has changed.
356 + markUpdate(workInProgress);
357 + }
358 + };
359 + } else {
360 + invariant(false, 'Persistent reconciler is disabled.');
361 + }
362 + } else {
363 + if (ReactFeatureFlags.enableNoopReconciler) {
364 + // No host operations
365 + updateHostContainer = function(workInProgress: Fiber) {
366 + // Noop
367 + };
368 + updateHostComponent = function(
369 + current: Fiber,
370 + workInProgress: Fiber,
371 + updatePayload: null | PL,
372 + type: T,
373 + oldProps: P,
374 + newProps: P,
375 + rootContainerInstance: C,
376 + ) {
377 + // Noop
378 + };
379 + updateHostText = function(
380 + current: Fiber,
381 + workInProgress: Fiber,
382 + oldText: string,
383 + newText: string,
384 + ) {
385 + // Noop
386 + };
387 + } else {
388 + invariant(false, 'Noop reconciler is disabled.');
389 + }
390 + }
391 +
392 function completeWork(
393 current: Fiber | null,
394 workInProgress: Fiber,
@@ -219,6 +431,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
431 // TODO: Delete this when we delete isMounted and findDOMNode.
432 workInProgress.effectTag &= ~Placement;
433 }
434 + updateHostContainer(workInProgress);
435 return null;
436 }
437 case HostComponent: {
@@ -244,13 +457,16 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
457 currentHostContext,
458 );
459
247 - // TODO: Type this specific to this type of component.
248 - workInProgress.updateQueue = (updatePayload: any);
249 - // If the update payload indicates that there is a change or if there
250 - // is a new ref we mark this as an update.
251 - if (updatePayload) {
252 - markUpdate(workInProgress);
253 - }
460 + updateHostComponent(
461 + current,
462 + workInProgress,
463 + updatePayload,
464 + type,
465 + oldProps,
466 + newProps,
467 + rootContainerInstance,
468 + );
469 +
470 if (current.ref !== workInProgress.ref) {
471 markRef(workInProgress);
472 }
@@ -325,9 +541,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
541 const oldText = current.memoizedProps;
542 // If we have an alternate, that means this is an update and we need
543 // to schedule a side-effect to do the updates.
328 - if (oldText !== newText) {
329 - markUpdate(workInProgress);
330 - }
544 + updateHostText(current, workInProgress, oldText, newText);
545 } else {
546 if (typeof newText !== 'string') {
547 invariant(
@@ -373,6 +587,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
587 return null;
588 case HostPortal:
589 popHostContainer(workInProgress);
590 + updateHostContainer(workInProgress);
591 return null;
592 // Error cases
593 case IndeterminateComponent:
packages/react-reconciler/src/ReactFiberHostContext.js
+2 -2
@@ -31,8 +31,8 @@ export type HostContext<C, CX> = {
31 resetHostContainer(): void,
32 };
33
34 -module.exports = function<T, P, I, TI, PI, C, CX, PL>(
35 - config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
34 +module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
35 + config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
36 ): HostContext<C, CX> {
37 const {getChildHostContext, getRootHostContext} = config;
38
packages/react-reconciler/src/ReactFiberHydrationContext.js
+2 -2
@@ -33,8 +33,8 @@ export type HydrationContext<C, CX> = {
33 popHydrationState(fiber: Fiber): boolean,
34 };
35
36 -module.exports = function<T, P, I, TI, PI, C, CX, PL>(
37 - config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
36 +module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
37 + config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
38 ): HydrationContext<C, CX> {
39 const {shouldSetTextContent, hydration} = config;
40
packages/react-reconciler/src/ReactFiberReconciler.js
+13 -17
@@ -47,7 +47,7 @@ export type Deadline = {
47 type OpaqueHandle = Fiber;
48 type OpaqueRoot = FiberRoot;
49
50 -export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
50 +export type HostConfig<T, P, I, TI, PI, C, CC, CX, PL> = {
51 getRootHostContext(rootContainerInstance: C): CX,
52 getChildHostContext(parentHostContext: CX, type: T, instance: C): CX,
53 getPublicInstance(instance: I | TI): PI,
@@ -100,7 +100,7 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
100 +hydration?: HydrationHostConfig<T, P, I, TI, C, CX, PL>,
101
102 +mutation?: MutableUpdatesHostConfig<T, P, I, TI, C, PL>,
103 - +persistence?: PersistentUpdatesHostConfig<T, P, I, C, CX, PL>,
103 + +persistence?: PersistentUpdatesHostConfig<T, P, I, TI, C, CC, PL>,
104 };
105
106 type MutableUpdatesHostConfig<T, P, I, TI, C, PL> = {
@@ -132,28 +132,24 @@ type MutableUpdatesHostConfig<T, P, I, TI, C, PL> = {
132 removeChildFromContainer(container: C, child: I | TI): void,
133 };
134
135 -type PersistentUpdatesHostConfig<T, P, I, C, CX, PL> = {
135 +type PersistentUpdatesHostConfig<T, P, I, TI, C, CC, PL> = {
136 cloneInstance(
137 instance: I,
138 - updatePayload: PL,
139 - type: T,
140 - oldProps: P,
141 - newProps: P,
142 - internalInstanceHandle: OpaqueHandle,
143 - keepChildren: boolean,
144 - ): I,
145 - tryToReuseInstance(
146 - instance: I,
147 - updatePayload: PL,
138 + updatePayload: null | PL,
139 type: T,
140 oldProps: P,
141 newProps: P,
142 internalInstanceHandle: OpaqueHandle,
143 keepChildren: boolean,
144 + recyclableInstance: I,
145 ): I,
146
155 - createRootInstance(rootContainerInstance: C, hostContext: CX): I,
156 - commitRootInstance(rootInstance: I): void,
147 + createContainerChildSet(container: C): CC,
148 +
149 + appendChildToContainerChildSet(childSet: CC, child: I | TI): void,
150 + finalizeContainerChildren(container: C, newChildren: CC): void,
151 +
152 + replaceContainerChildren(container: C, newChildren: CC): void,
153 };
154
155 type HydrationHostConfig<T, P, I, TI, C, CX, PL> = {
@@ -257,8 +253,8 @@ function getContextForSubtree(
253 : parentContext;
254 }
255
260 -module.exports = function<T, P, I, TI, PI, C, CX, PL>(
261 - config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
256 +module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
257 + config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
258 ): Reconciler<C, I, TI> {
259 var {getPublicInstance} = config;
260
packages/react-reconciler/src/ReactFiberRoot.js
+3
@@ -17,6 +17,8 @@ const {createHostRootFiber} = require('ReactFiber');
17 export type FiberRoot = {
18 // Any additional information from the host associated with this root.
19 containerInfo: any,
20 + // Used only by persistent updates.
21 + pendingChildren: any,
22 // The currently active root fiber. This is the mutable root of the tree.
23 current: Fiber,
24 // Determines if this root has already been added to the schedule for work.
@@ -40,6 +42,7 @@ exports.createFiberRoot = function(
42 const root = {
43 current: uninitializedFiber,
44 containerInfo: containerInfo,
45 + pendingChildren: null,
46 isScheduled: false,
47 nextScheduledRoot: null,
48 context: null,
packages/react-reconciler/src/ReactFiberScheduler.js
+2 -2
@@ -153,8 +153,8 @@ if (__DEV__) {
153
154 var timeHeuristicForUnitOfWork = 1;
155
156 -module.exports = function<T, P, I, TI, PI, C, CX, PL>(
157 - config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
156 +module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
157 + config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
158 ) {
159 const hostContext = ReactFiberHostContext(config);
160 const hydrationContext: HydrationContext<C, CX> = ReactFiberHydrationContext(
packages/react-reconciler/src/__tests__/ReactPersistent-test.js new
+207
@@ -0,0 +1,207 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
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 + */
9 +
10 +'use strict';
11 +
12 +var React;
13 +var ReactNoop;
14 +let ReactPortal;
15 +
16 +describe('ReactPersistent', () => {
17 + beforeEach(() => {
18 + jest.resetModules();
19 +
20 + const ReactFeatureFlags = require('ReactFeatureFlags');
21 + ReactFeatureFlags.enableMutableReconciler = false;
22 + ReactFeatureFlags.enablePersistentReconciler = true;
23 + ReactFeatureFlags.enableNoopReconciler = false;
24 +
25 + React = require('react');
26 + ReactNoop = require('react-noop-renderer');
27 + ReactPortal = require('ReactPortal');
28 + });
29 +
30 + const DEFAULT_ROOT_ID = 'persistent-test';
31 +
32 + function render(element) {
33 + ReactNoop.renderToPersistentRootWithID(element, DEFAULT_ROOT_ID);
34 + }
35 +
36 + function div(...children) {
37 + children = children.map(c => (typeof c === 'string' ? {text: c} : c));
38 + return {type: 'div', children, prop: undefined};
39 + }
40 +
41 + function span(prop) {
42 + return {type: 'span', children: [], prop};
43 + }
44 +
45 + function getChildren() {
46 + return ReactNoop.getChildren(DEFAULT_ROOT_ID);
47 + }
48 +
49 + it('can update child nodes of a host instance', () => {
50 + function Bar(props) {
51 + return <span>{props.text}</span>;
52 + }
53 +
54 + function Foo(props) {
55 + return (
56 + <div>
57 + <Bar text={props.text} />
58 + {props.text === 'World' ? <Bar text={props.text} /> : null}
59 + </div>
60 + );
61 + }
62 +
63 + render(<Foo text="Hello" />);
64 + ReactNoop.flush();
65 + var originalChildren = getChildren();
66 + expect(originalChildren).toEqual([div(span())]);
67 +
68 + render(<Foo text="World" />);
69 + ReactNoop.flush();
70 + var newChildren = getChildren();
71 + expect(newChildren).toEqual([div(span(), span())]);
72 +
73 + expect(originalChildren).toEqual([div(span())]);
74 + });
75 +
76 + it('can reuse child nodes between updates', () => {
77 + function Baz(props) {
78 + return <span prop={props.text} />;
79 + }
80 + class Bar extends React.Component {
81 + shouldComponentUpdate(newProps) {
82 + return false;
83 + }
84 + render() {
85 + return <Baz text={this.props.text} />;
86 + }
87 + }
88 + function Foo(props) {
89 + return (
90 + <div>
91 + <Bar text={props.text} />
92 + {props.text === 'World' ? <Bar text={props.text} /> : null}
93 + </div>
94 + );
95 + }
96 +
97 + render(<Foo text="Hello" />);
98 + ReactNoop.flush();
99 + var originalChildren = getChildren();
100 + expect(originalChildren).toEqual([div(span('Hello'))]);
101 +
102 + render(<Foo text="World" />);
103 + ReactNoop.flush();
104 + var newChildren = getChildren();
105 + expect(newChildren).toEqual([div(span('Hello'), span('World'))]);
106 +
107 + expect(originalChildren).toEqual([div(span('Hello'))]);
108 +
109 + // Reused node should have reference equality
110 + expect(newChildren[0].children[0]).toBe(originalChildren[0].children[0]);
111 + });
112 +
113 + it('can update child text nodes', () => {
114 + function Foo(props) {
115 + return (
116 + <div>
117 + {props.text}
118 + <span />
119 + </div>
120 + );
121 + }
122 +
123 + render(<Foo text="Hello" />);
124 + ReactNoop.flush();
125 + var originalChildren = getChildren();
126 + expect(originalChildren).toEqual([div('Hello', span())]);
127 +
128 + render(<Foo text="World" />);
129 + ReactNoop.flush();
130 + var newChildren = getChildren();
131 + expect(newChildren).toEqual([div('World', span())]);
132 +
133 + expect(originalChildren).toEqual([div('Hello', span())]);
134 + });
135 +
136 + it('supports portals', () => {
137 + function Parent(props) {
138 + return <div>{props.children}</div>;
139 + }
140 +
141 + function BailoutSpan() {
142 + return <span />;
143 + }
144 +
145 + class BailoutTest extends React.Component {
146 + shouldComponentUpdate() {
147 + return false;
148 + }
149 + render() {
150 + return <BailoutSpan />;
151 + }
152 + }
153 +
154 + function Child(props) {
155 + return <div><BailoutTest />{props.children}</div>;
156 + }
157 + const portalContainer = {rootID: 'persistent-portal-test', children: []};
158 + const emptyPortalChildSet = portalContainer.children;
159 + render(
160 + <Parent>
161 + {ReactPortal.createPortal(<Child />, portalContainer, null)}
162 + </Parent>,
163 + );
164 + ReactNoop.flush();
165 +
166 + expect(emptyPortalChildSet).toEqual([]);
167 +
168 + var originalChildren = getChildren();
169 + expect(originalChildren).toEqual([div()]);
170 + var originalPortalChildren = portalContainer.children;
171 + expect(originalPortalChildren).toEqual([div(span())]);
172 +
173 + render(
174 + <Parent>
175 + {ReactPortal.createPortal(
176 + <Child>Hello {'World'}</Child>,
177 + portalContainer,
178 + null,
179 + )}
180 + </Parent>,
181 + );
182 + ReactNoop.flush();
183 +
184 + var newChildren = getChildren();
185 + expect(newChildren).toEqual([div()]);
186 + var newPortalChildren = portalContainer.children;
187 + expect(newPortalChildren).toEqual([div(span(), 'Hello ', 'World')]);
188 +
189 + expect(originalChildren).toEqual([div()]);
190 + expect(originalPortalChildren).toEqual([div(span())]);
191 +
192 + // Reused portal children should have reference equality
193 + expect(newPortalChildren[0].children[0]).toBe(
194 + originalPortalChildren[0].children[0],
195 + );
196 +
197 + // Deleting the Portal, should clear its children
198 + render(<Parent />);
199 + ReactNoop.flush();
200 +
201 + var clearedPortalChildren = portalContainer.children;
202 + expect(clearedPortalChildren).toEqual([]);
203 +
204 + // The original is unchanged.
205 + expect(newPortalChildren).toEqual([div(span(), 'Hello ', 'World')]);
206 + });
207 +});
packages/shared/src/utils/ReactFeatureFlags.js
+9
@@ -13,11 +13,20 @@
13 export type FeatureFlags = {|
14 enableAsyncSubtreeAPI: boolean,
15 enableAsyncSchedulingByDefaultInReactDOM: boolean,
16 + enableMutatingReconciler: boolean,
17 + enableNoopReconciler: boolean,
18 + enablePersistentReconciler: boolean,
19 |};
20
21 var ReactFeatureFlags: FeatureFlags = {
22 enableAsyncSubtreeAPI: true,
23 enableAsyncSchedulingByDefaultInReactDOM: false,
24 + // Mutating mode (React DOM, React ART, React Native):
25 + enableMutatingReconciler: true,
26 + // Experimental noop mode (currently unused):
27 + enableNoopReconciler: false,
28 + // Experimental persistent mode (CS):
29 + enablePersistentReconciler: false,
30 };
31
32 if (__DEV__) {
scripts/error-codes/codes.json
+8 -1
@@ -233,5 +233,12 @@
233 "231": "Expected `%s` listener to be a function, instead got a value of `%s` type.",
234 "232": "_processChildContext is not available in React 16+. This likely means you have multiple copies of React and are attempting to nest a React 15 tree inside a React 16 tree using unstable_renderSubtreeIntoContainer, which isn't supported. Try to make sure you have only one copy of React (and ideally, switch to ReactDOM.createPortal).",
235 "233": "Unsupported top level event type \"%s\" dispatched",
236 - "234": "Event cannot be both direct and bubbling: %s"
236 + "234": "Event cannot be both direct and bubbling: %s",
237 + "235": "Persistent reconciler is disabled.",
238 + "236": "Noop reconciler is disabled.",
239 + "237": "Mutating reconciler is disabled.",
240 + "238": "Task updates can only be scheduled as a nested update or inside batchedUpdates. This error is likely caused by a bug in React. Please file an issue.",
241 + "239": "Measure not implemented yet",
242 + "240": "Text components are not supported for now.",
243 + "241": "Text components are not yet supported."
244 }
scripts/rollup/bundles.js
+1
@@ -323,6 +323,7 @@ const bundles = [
323 label: 'native-cs-fiber',
324 manglePropertiesOnProd: false,
325 name: 'react-native-cs-renderer',
326 + featureFlags: 'packages/react-cs-renderer/src/ReactNativeCSFeatureFlags',
327 paths: [
328 'packages/react-native-renderer/**/*.js', // This is used since we reuse the error dialog code
329 'packages/react-cs-renderer/**/*.js',
scripts/rollup/results.json
+48 -48
@@ -25,28 +25,28 @@
25 "gzip": 6709
26 },
27 "react-dom.development.js (UMD_DEV)": {
28 - "size": 622438,
29 - "gzip": 143476
28 + "size": 632002,
29 + "gzip": 144937
30 },
31 "react-dom.production.min.js (UMD_PROD)": {
32 - "size": 100793,
33 - "gzip": 31792
32 + "size": 100474,
33 + "gzip": 31680
34 },
35 "react-dom.development.js (NODE_DEV)": {
36 - "size": 584733,
37 - "gzip": 134780
36 + "size": 594283,
37 + "gzip": 136223
38 },
39 "react-dom.production.min.js (NODE_PROD)": {
40 - "size": 105201,
41 - "gzip": 33053
40 + "size": 107016,
41 + "gzip": 33539
42 },
43 "ReactDOMFiber-dev.js (FB_DEV)": {
44 - "size": 581810,
45 - "gzip": 134178
44 + "size": 591222,
45 + "gzip": 135569
46 },
47 "ReactDOMFiber-prod.js (FB_PROD)": {
48 - "size": 413106,
49 - "gzip": 92244
48 + "size": 421158,
49 + "gzip": 93551
50 },
51 "react-dom-test-utils.development.js (NODE_DEV)": {
52 "size": 41688,
@@ -113,44 +113,44 @@
113 "gzip": 6214
114 },
115 "react-art.development.js (UMD_DEV)": {
116 - "size": 368779,
117 - "gzip": 81552
116 + "size": 378343,
117 + "gzip": 83009
118 },
119 "react-art.production.min.js (UMD_PROD)": {
120 - "size": 82740,
121 - "gzip": 25652
120 + "size": 82432,
121 + "gzip": 25592
122 },
123 "react-art.development.js (NODE_DEV)": {
124 - "size": 293112,
125 - "gzip": 62358
124 + "size": 302698,
125 + "gzip": 63826
126 },
127 "react-art.production.min.js (NODE_PROD)": {
128 - "size": 52107,
129 - "gzip": 16388
128 + "size": 53897,
129 + "gzip": 16869
130 },
131 "ReactARTFiber-dev.js (FB_DEV)": {
132 - "size": 291853,
133 - "gzip": 62333
132 + "size": 301301,
133 + "gzip": 63733
134 },
135 "ReactARTFiber-prod.js (FB_PROD)": {
136 - "size": 217374,
137 - "gzip": 45136
136 + "size": 225462,
137 + "gzip": 46449
138 },
139 "ReactNativeFiber-dev.js (RN_DEV)": {
140 - "size": 278956,
141 - "gzip": 48430
140 + "size": 285864,
141 + "gzip": 49285
142 },
143 "ReactNativeFiber-prod.js (RN_PROD)": {
144 - "size": 217084,
145 - "gzip": 37619
144 + "size": 223648,
145 + "gzip": 38454
146 },
147 "react-test-renderer.development.js (NODE_DEV)": {
148 - "size": 296835,
149 - "gzip": 62765
148 + "size": 306386,
149 + "gzip": 64218
150 },
151 "ReactTestRendererFiber-dev.js (FB_DEV)": {
152 - "size": 295566,
153 - "gzip": 62732
152 + "size": 304979,
153 + "gzip": 64135
154 },
155 "react-test-renderer-shallow.development.js (NODE_DEV)": {
156 "size": 9370,
@@ -161,8 +161,8 @@
161 "gzip": 2253
162 },
163 "react-noop-renderer.development.js (NODE_DEV)": {
164 - "size": 284424,
165 - "gzip": 59683
164 + "size": 295594,
165 + "gzip": 61405
166 },
167 "react-dom-server.development.js (UMD_DEV)": {
168 "size": 120897,
@@ -189,16 +189,16 @@
189 "gzip": 7520
190 },
191 "ReactNativeRTFiber-dev.js (RN_DEV)": {
192 - "size": 210860,
193 - "gzip": 35891
192 + "size": 217767,
193 + "gzip": 36738
194 },
195 "ReactNativeRTFiber-prod.js (RN_PROD)": {
196 - "size": 158763,
197 - "gzip": 26631
196 + "size": 165326,
197 + "gzip": 27464
198 },
199 "react-test-renderer.production.min.js (NODE_PROD)": {
200 - "size": 53651,
201 - "gzip": 16653
200 + "size": 55449,
201 + "gzip": 17169
202 },
203 "react-test-renderer-shallow.production.min.js (NODE_PROD)": {
204 "size": 4630,
@@ -209,20 +209,20 @@
209 "gzip": 4241
210 },
211 "react-reconciler.development.js (NODE_DEV)": {
212 - "size": 271721,
213 - "gzip": 56840
212 + "size": 281271,
213 + "gzip": 58283
214 },
215 "react-reconciler.production.min.js (NODE_PROD)": {
216 - "size": 37669,
217 - "gzip": 11733
216 + "size": 37658,
217 + "gzip": 11762
218 },
219 "ReactNativeCSFiber-dev.js (RN_DEV)": {
220 - "size": 203248,
221 - "gzip": 34141
220 + "size": 210188,
221 + "gzip": 34969
222 },
223 "ReactNativeCSFiber-prod.js (RN_PROD)": {
224 - "size": 153658,
225 - "gzip": 25401
224 + "size": 160321,
225 + "gzip": 26276
226 }
227 }
228 }
\ No newline at end of file