Skip to content

Commit c73400f

Browse files
committed
add a Cloned flag to indicate a required clone in persistent mode
1 parent 8416ebe commit c73400f

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
@@ -121,6 +121,12 @@ export const passChildrenWhenCloningPersistedNodes = false;
121121

122122
export const enableServerComponentLogs = __EXPERIMENTAL__;
123123

124+
/**
125+
* Enables a new Fiber flag used in persisted mode to reduce the number
126+
* of cloned host components.
127+
*/
128+
export const enablePersistedModeClonedFlag = false;
129+
124130
export const enableAddPropertiesFastPath = false;
125131

126132
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
@@ -22,5 +22,6 @@ export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
2222
export const disableDefaultPropsExceptForClasses = __VARIANT__;
2323
export const enableAddPropertiesFastPath = __VARIANT__;
2424
export const enableFastJSX = __VARIANT__;
25+
export const enablePersistedModeClonedFlag = __VARIANT__;
2526
export const enableShallowPropDiffing = __VARIANT__;
2627
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
@@ -24,6 +24,7 @@ export const {
2424
disableDefaultPropsExceptForClasses,
2525
enableAddPropertiesFastPath,
2626
enableFastJSX,
27+
enablePersistedModeClonedFlag,
2728
enableShallowPropDiffing,
2829
passChildrenWhenCloningPersistedNodes,
2930
} = dynamicFlags;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const enableLegacyFBSupport = false;
5656
export const enableLegacyHidden = false;
5757
export const enableNoCloningMemoCache = false;
5858
export const enableOwnerStacks = __EXPERIMENTAL__;
59+
export const enablePersistedModeClonedFlag = false;
5960
export const enablePostpone = false;
6061
export const enableReactTestRendererWarning = false;
6162
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
@@ -48,6 +48,7 @@ export const enableLegacyFBSupport = false;
4848
export const enableLegacyHidden = false;
4949
export const enableNoCloningMemoCache = false;
5050
export const enableOwnerStacks = false;
51+
export const enablePersistedModeClonedFlag = false;
5152
export const enablePostpone = false;
5253
export const enableProfilerCommitHooks = __PROFILE__;
5354
export const enableProfilerNestedUpdatePhase = __PROFILE__;

0 commit comments

Comments
 (0)