Skip to content

Commit b18abbb

Browse files
committed
add a Cloned flag to indicate a required clone in persistent mode
1 parent 163365a commit b18abbb

12 files changed

+180
-12
lines changed

packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,130 @@ describe('ReactFabric', () => {
279279
expect(nativeFabricUIManager.completeRoot).toBeCalled();
280280
});
281281

282+
// @gate enablePersistedModeClonedFlag
283+
it('should not clone nodes when layout effects are used', async () => {
284+
const View = createReactNativeComponentClass('RCTView', () => ({
285+
validAttributes: {foo: true},
286+
uiViewClassName: 'RCTView',
287+
}));
288+
289+
const ComponentWithEffect = () => {
290+
React.useLayoutEffect(() => {});
291+
return null;
292+
};
293+
294+
await act(() =>
295+
ReactFabric.render(
296+
<View>
297+
<ComponentWithEffect />
298+
</View>,
299+
11,
300+
),
301+
);
302+
expect(nativeFabricUIManager.completeRoot).toBeCalled();
303+
jest.clearAllMocks();
304+
305+
await act(() =>
306+
ReactFabric.render(
307+
<View>
308+
<ComponentWithEffect />
309+
</View>,
310+
11,
311+
),
312+
);
313+
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
314+
expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled();
315+
expect(nativeFabricUIManager.cloneNodeWithNewProps).not.toBeCalled();
316+
expect(
317+
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
318+
).not.toBeCalled();
319+
expect(nativeFabricUIManager.completeRoot).not.toBeCalled();
320+
});
321+
322+
// @gate enablePersistedModeClonedFlag
323+
it('should not clone nodes when insertion effects are used', async () => {
324+
const View = createReactNativeComponentClass('RCTView', () => ({
325+
validAttributes: {foo: true},
326+
uiViewClassName: 'RCTView',
327+
}));
328+
329+
const ComponentWithRef = () => {
330+
React.useInsertionEffect(() => {});
331+
return null;
332+
};
333+
334+
await act(() =>
335+
ReactFabric.render(
336+
<View>
337+
<ComponentWithRef />
338+
</View>,
339+
11,
340+
),
341+
);
342+
expect(nativeFabricUIManager.completeRoot).toBeCalled();
343+
jest.clearAllMocks();
344+
345+
await act(() =>
346+
ReactFabric.render(
347+
<View>
348+
<ComponentWithRef />
349+
</View>,
350+
11,
351+
),
352+
);
353+
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
354+
expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled();
355+
expect(nativeFabricUIManager.cloneNodeWithNewProps).not.toBeCalled();
356+
expect(
357+
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
358+
).not.toBeCalled();
359+
expect(nativeFabricUIManager.completeRoot).not.toBeCalled();
360+
});
361+
362+
// @gate enablePersistedModeClonedFlag
363+
it('should not clone nodes when useImperativeHandle is used', async () => {
364+
const View = createReactNativeComponentClass('RCTView', () => ({
365+
validAttributes: {foo: true},
366+
uiViewClassName: 'RCTView',
367+
}));
368+
369+
const ComponentWithImperativeHandle = props => {
370+
React.useImperativeHandle(props.ref, () => ({greet: () => 'hello'}));
371+
return null;
372+
};
373+
374+
const ref = React.createRef();
375+
376+
await act(() =>
377+
ReactFabric.render(
378+
<View>
379+
<ComponentWithImperativeHandle ref={ref} />
380+
</View>,
381+
11,
382+
),
383+
);
384+
expect(nativeFabricUIManager.completeRoot).toBeCalled();
385+
expect(ref.current.greet()).toBe('hello');
386+
jest.clearAllMocks();
387+
388+
await act(() =>
389+
ReactFabric.render(
390+
<View>
391+
<ComponentWithImperativeHandle ref={ref} />
392+
</View>,
393+
11,
394+
),
395+
);
396+
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
397+
expect(nativeFabricUIManager.cloneNodeWithNewChildren).not.toBeCalled();
398+
expect(nativeFabricUIManager.cloneNodeWithNewProps).not.toBeCalled();
399+
expect(
400+
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
401+
).not.toBeCalled();
402+
expect(nativeFabricUIManager.completeRoot).not.toBeCalled();
403+
expect(ref.current.greet()).toBe('hello');
404+
});
405+
282406
it('should call dispatchCommand for native refs', async () => {
283407
const View = createReactNativeComponentClass('RCTView', () => ({
284408
validAttributes: {foo: true},

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import type {
4242
import {
4343
alwaysThrottleRetries,
4444
enableCreateEventHandleAPI,
45+
enablePersistedModeClonedFlag,
4546
enableProfilerTimer,
4647
enableProfilerCommitHooks,
4748
enableProfilerNestedUpdatePhase,
@@ -98,6 +99,7 @@ import {
9899
ShouldSuspendCommit,
99100
MaySuspendCommit,
100101
FormReset,
102+
Cloned,
101103
} from './ReactFiberFlags';
102104
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
103105
import {runWithFiberInDEV} from './ReactCurrentFiber';
@@ -2509,7 +2511,10 @@ function recursivelyTraverseMutationEffects(
25092511
}
25102512
}
25112513

2512-
if (parentFiber.subtreeFlags & MutationMask) {
2514+
if (
2515+
parentFiber.subtreeFlags &
2516+
(enablePersistedModeClonedFlag ? MutationMask | Cloned : MutationMask)
2517+
) {
25132518
let child = parentFiber.child;
25142519
while (child !== null) {
25152520
if (__DEV__) {

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
enableLegacyHidden,
3636
enableSuspenseCallback,
3737
enableScopeAPI,
38+
enablePersistedModeClonedFlag,
3839
enableProfilerTimer,
3940
enableCache,
4041
enableTransitionTracing,
@@ -90,6 +91,7 @@ import {
9091
MaySuspendCommit,
9192
ScheduleRetry,
9293
ShouldSuspendCommit,
94+
Cloned,
9395
} from './ReactFiberFlags';
9496

9597
import {
@@ -182,6 +184,16 @@ function markUpdate(workInProgress: Fiber) {
182184
workInProgress.flags |= Update;
183185
}
184186

187+
/**
188+
* Tag the fiber with Cloned in persistent mode to signal that
189+
* it received an update that requires a clone of the tree above.
190+
*/
191+
function markCloned(workInProgress: Fiber) {
192+
if (supportsPersistence && enablePersistedModeClonedFlag) {
193+
workInProgress.flags |= Cloned;
194+
}
195+
}
196+
185197
/**
186198
* In persistent mode, return whether this update needs to clone the subtree.
187199
*/
@@ -199,9 +211,12 @@ function doesRequireClone(current: null | Fiber, completedWork: Fiber) {
199211
// then we only have to check the `completedWork.subtreeFlags`.
200212
let child = completedWork.child;
201213
while (child !== null) {
214+
const checkedFlags = enablePersistedModeClonedFlag
215+
? Cloned | Visibility | Placement
216+
: MutationMask;
202217
if (
203-
(child.flags & MutationMask) !== NoFlags ||
204-
(child.subtreeFlags & MutationMask) !== NoFlags
218+
(child.flags & checkedFlags) !== NoFlags ||
219+
(child.subtreeFlags & checkedFlags) !== NoFlags
205220
) {
206221
return true;
207222
}
@@ -450,6 +465,7 @@ function updateHostComponent(
450465

451466
let newChildSet = null;
452467
if (requiresClone && passChildrenWhenCloningPersistedNodes) {
468+
markCloned(workInProgress);
453469
newChildSet = createContainerChildSet();
454470
// If children might have changed, we have to add them all to the set.
455471
appendAllChildrenToContainer(
@@ -473,6 +489,8 @@ function updateHostComponent(
473489
// Note that this might release a previous clone.
474490
workInProgress.stateNode = currentInstance;
475491
return;
492+
} else {
493+
markCloned(workInProgress);
476494
}
477495

478496
// Certain renderers require commit-time effects for initial mount.
@@ -485,12 +503,14 @@ function updateHostComponent(
485503
}
486504
workInProgress.stateNode = newInstance;
487505
if (!requiresClone) {
488-
// If there are no other effects in this tree, we need to flag this node as having one.
489-
// Even though we're not going to use it for anything.
490-
// Otherwise parents won't know that there are new children to propagate upwards.
491-
markUpdate(workInProgress);
506+
if (!enablePersistedModeClonedFlag) {
507+
// If there are no other effects in this tree, we need to flag this node as having one.
508+
// Even though we're not going to use it for anything.
509+
// Otherwise parents won't know that there are new children to propagate upwards.
510+
markUpdate(workInProgress);
511+
}
492512
} else if (!passChildrenWhenCloningPersistedNodes) {
493-
// If children might have changed, we have to add them all to the set.
513+
// If children have changed, we have to add them all to the set.
494514
appendAllChildren(
495515
newInstance,
496516
workInProgress,
@@ -618,15 +638,18 @@ function updateHostText(
618638
// If the text content differs, we'll create a new text instance for it.
619639
const rootContainerInstance = getRootHostContainer();
620640
const currentHostContext = getHostContext();
641+
markCloned(workInProgress);
621642
workInProgress.stateNode = createTextInstance(
622643
newText,
623644
rootContainerInstance,
624645
currentHostContext,
625646
workInProgress,
626647
);
627-
// We'll have to mark it as having an effect, even though we won't use the effect for anything.
628-
// This lets the parents know that at least one of their children has changed.
629-
markUpdate(workInProgress);
648+
if (!enablePersistedModeClonedFlag) {
649+
// We'll have to mark it as having an effect, even though we won't use the effect for anything.
650+
// This lets the parents know that at least one of their children has changed.
651+
markUpdate(workInProgress);
652+
}
630653
} else {
631654
workInProgress.stateNode = current.stateNode;
632655
}
@@ -1229,6 +1252,7 @@ function completeWork(
12291252
);
12301253
// TODO: For persistent renderers, we should pass children as part
12311254
// of the initial instance creation
1255+
markCloned(workInProgress);
12321256
appendAllChildren(instance, workInProgress, false, false);
12331257
workInProgress.stateNode = instance;
12341258

@@ -1284,6 +1308,7 @@ function completeWork(
12841308
if (wasHydrated) {
12851309
prepareToHydrateHostTextInstance(workInProgress);
12861310
} else {
1311+
markCloned(workInProgress);
12871312
workInProgress.stateNode = createTextInstance(
12881313
newText,
12891314
rootContainerInstance,

packages/react-reconciler/src/ReactFiberFlags.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const Hydrating = /* */ 0b0000000000000001000000000000
2020

2121
// You can change the rest (and add more).
2222
export const Update = /* */ 0b0000000000000000000000000100;
23-
/* Skipped value: 0b0000000000000000000000001000; */
23+
export const Cloned = /* */ 0b0000000000000000000000001000;
2424

2525
export const ChildDeletion = /* */ 0b0000000000000000000000010000;
2626
export const ContentReset = /* */ 0b0000000000000000000000100000;

packages/shared/ReactFeatureFlags.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ export const passChildrenWhenCloningPersistedNodes = false;
126126

127127
export const enableServerComponentLogs = __EXPERIMENTAL__;
128128

129+
/**
130+
* Enables a new Fiber flag used in persisted mode to reduce the number
131+
* of cloned host components.
132+
*/
133+
export const enablePersistedModeClonedFlag = false;
134+
129135
export const enableAddPropertiesFastPath = false;
130136

131137
export const enableOwnerStacks = __EXPERIMENTAL__;

packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ export const alwaysThrottleRetries = __VARIANT__;
2121
export const enableAddPropertiesFastPath = __VARIANT__;
2222
export const enableFastJSX = __VARIANT__;
2323
export const enableObjectFiber = __VARIANT__;
24+
export const enablePersistedModeClonedFlag = __VARIANT__;
2425
export const enableShallowPropDiffing = __VARIANT__;
2526
export const passChildrenWhenCloningPersistedNodes = __VARIANT__;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const {
2323
enableAddPropertiesFastPath,
2424
enableFastJSX,
2525
enableObjectFiber,
26+
enablePersistedModeClonedFlag,
2627
enableShallowPropDiffing,
2728
passChildrenWhenCloningPersistedNodes,
2829
} = dynamicFlags;

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const enableLegacyHidden = false;
5858
export const enableNoCloningMemoCache = false;
5959
export const enableObjectFiber = false;
6060
export const enableOwnerStacks = false;
61+
export const enablePersistedModeClonedFlag = false;
6162
export const enablePostpone = false;
6263
export const enableReactTestRendererWarning = false;
6364
export const enableRefAsProp = true;

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const enableAsyncActions = true;
7070
export const alwaysThrottleRetries = true;
7171

7272
export const passChildrenWhenCloningPersistedNodes = false;
73+
export const enablePersistedModeClonedFlag = false;
7374
export const enableUseDeferredValueInitialArg = __EXPERIMENTAL__;
7475
export const disableClientCache = true;
7576

packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const enableLegacyHidden = false;
5050
export const enableNoCloningMemoCache = false;
5151
export const enableObjectFiber = false;
5252
export const enableOwnerStacks = false;
53+
export const enablePersistedModeClonedFlag = false;
5354
export const enablePostpone = false;
5455
export const enableProfilerCommitHooks = __PROFILE__;
5556
export const enableProfilerNestedUpdatePhase = __PROFILE__;

0 commit comments

Comments
 (0)