Skip to content

Commit af9b87b

Browse files
javacheAndyPengc12
authored andcommitted
[Fabric] Pass children when cloning (facebook#27458)
## Summary Currently when cloning nodes in Fabric, we reset a node's children on each clone, and then repeatedly call appendChild to restore the previous list of children (even if it was quasi-identical to before). This causes unnecessary invalidation of the layout state in Fabric's ShadowNode data (which in turn may require additional yoga clones) and extra JSI calls. This PR adds a feature flag to pass in the children as part of the clone call, so Fabric always has a complete view of the node that's being mutated. This feature flag requires matching changes in the react-native repo: facebook/react-native#39817 ## How did you test this change? Unit test added demonstrates the new behaviour ``` yarn test -r www-modern ReactFabric-test yarn test ReactFabric-test.internal ``` Tested a manual sync into React Native and verified core surfaces render correctly.
1 parent 49858ba commit af9b87b

16 files changed

+178
-55
lines changed

packages/react-native-renderer/src/ReactFiberConfigFabric.js

+37-13
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ const {
4747
unstable_getCurrentEventPriority: fabricGetCurrentEventPriority,
4848
} = nativeFabricUIManager;
4949

50-
import {useMicrotasksForSchedulingInFabric} from 'shared/ReactFeatureFlags';
50+
import {
51+
useMicrotasksForSchedulingInFabric,
52+
passChildrenWhenCloningPersistedNodes,
53+
} from 'shared/ReactFeatureFlags';
5154

5255
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
5356

@@ -87,7 +90,7 @@ export type TextInstance = {
8790
export type HydratableInstance = Instance | TextInstance;
8891
export type PublicInstance = ReactNativePublicInstance;
8992
export type Container = number;
90-
export type ChildSet = Object;
93+
export type ChildSet = Object | Array<Node>;
9194
export type HostContext = $ReadOnly<{
9295
isInAParentText: boolean,
9396
}>;
@@ -346,9 +349,8 @@ export function cloneInstance(
346349
type: string,
347350
oldProps: Props,
348351
newProps: Props,
349-
internalInstanceHandle: InternalInstanceHandle,
350352
keepChildren: boolean,
351-
recyclableInstance: null | Instance,
353+
newChildSet: ?ChildSet,
352354
): Instance {
353355
const viewConfig = instance.canonical.viewConfig;
354356
const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
@@ -367,12 +369,26 @@ export function cloneInstance(
367369
return instance;
368370
}
369371
} else {
370-
if (updatePayload !== null) {
371-
clone = cloneNodeWithNewChildrenAndProps(node, updatePayload);
372+
// If passChildrenWhenCloningPersistedNodes is enabled, children will be non-null
373+
if (newChildSet != null) {
374+
if (updatePayload !== null) {
375+
clone = cloneNodeWithNewChildrenAndProps(
376+
node,
377+
newChildSet,
378+
updatePayload,
379+
);
380+
} else {
381+
clone = cloneNodeWithNewChildren(node, newChildSet);
382+
}
372383
} else {
373-
clone = cloneNodeWithNewChildren(node);
384+
if (updatePayload !== null) {
385+
clone = cloneNodeWithNewChildrenAndProps(node, updatePayload);
386+
} else {
387+
clone = cloneNodeWithNewChildren(node);
388+
}
374389
}
375390
}
391+
376392
return {
377393
node: clone,
378394
canonical: instance.canonical,
@@ -383,7 +399,6 @@ export function cloneHiddenInstance(
383399
instance: Instance,
384400
type: string,
385401
props: Props,
386-
internalInstanceHandle: InternalInstanceHandle,
387402
): Instance {
388403
const viewConfig = instance.canonical.viewConfig;
389404
const node = instance.node;
@@ -400,20 +415,27 @@ export function cloneHiddenInstance(
400415
export function cloneHiddenTextInstance(
401416
instance: Instance,
402417
text: string,
403-
internalInstanceHandle: InternalInstanceHandle,
404418
): TextInstance {
405419
throw new Error('Not yet implemented.');
406420
}
407421

408-
export function createContainerChildSet(container: Container): ChildSet {
409-
return createChildNodeSet(container);
422+
export function createContainerChildSet(): ChildSet {
423+
if (passChildrenWhenCloningPersistedNodes) {
424+
return [];
425+
} else {
426+
return createChildNodeSet();
427+
}
410428
}
411429

412430
export function appendChildToContainerChildSet(
413431
childSet: ChildSet,
414432
child: Instance | TextInstance,
415433
): void {
416-
appendChildNodeToSet(childSet, child.node);
434+
if (passChildrenWhenCloningPersistedNodes) {
435+
childSet.push(child.node);
436+
} else {
437+
appendChildNodeToSet(childSet, child.node);
438+
}
417439
}
418440

419441
export function finalizeContainerChildren(
@@ -426,7 +448,9 @@ export function finalizeContainerChildren(
426448
export function replaceContainerChildren(
427449
container: Container,
428450
newChildren: ChildSet,
429-
): void {}
451+
): void {
452+
// Noop - children will be replaced in finalizeContainerChildren
453+
}
430454

431455
export function getInstanceFromNode(node: any): empty {
432456
throw new Error('Not yet implemented.');

packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,15 @@ const RCTFabricUIManager = {
7070
children: node.children,
7171
};
7272
}),
73-
cloneNodeWithNewChildren: jest.fn(function cloneNodeWithNewChildren(node) {
73+
cloneNodeWithNewChildren: jest.fn(function cloneNodeWithNewChildren(
74+
node,
75+
children,
76+
) {
7477
return {
7578
reactTag: node.reactTag,
7679
viewName: node.viewName,
7780
props: node.props,
78-
children: [],
81+
children: children ?? [],
7982
};
8083
}),
8184
cloneNodeWithNewProps: jest.fn(function cloneNodeWithNewProps(
@@ -91,11 +94,17 @@ const RCTFabricUIManager = {
9194
}),
9295
cloneNodeWithNewChildrenAndProps: jest.fn(
9396
function cloneNodeWithNewChildrenAndProps(node, newPropsDiff) {
97+
let children = [];
98+
if (arguments.length === 3) {
99+
children = newPropsDiff;
100+
newPropsDiff = arguments[2];
101+
}
102+
94103
return {
95104
reactTag: node.reactTag,
96105
viewName: node.viewName,
97106
props: {...node.props, ...newPropsDiff},
98-
children: [],
107+
children,
99108
};
100109
},
101110
),

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

+54-1
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,13 @@ describe('ReactFabric', () => {
210210
11,
211211
);
212212
});
213+
const argIndex = gate(flags => flags.passChildrenWhenCloningPersistedNodes)
214+
? 2
215+
: 1;
213216
expect(
214-
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps.mock.calls[0][1],
217+
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps.mock.calls[0][
218+
argIndex
219+
],
215220
).toEqual({
216221
foo: 'b',
217222
});
@@ -220,6 +225,54 @@ describe('ReactFabric', () => {
220225
).toMatchSnapshot();
221226
});
222227

228+
it('should not clone nodes without children when updating props', async () => {
229+
const View = createReactNativeComponentClass('RCTView', () => ({
230+
validAttributes: {foo: true},
231+
uiViewClassName: 'RCTView',
232+
}));
233+
234+
const Component = ({foo}) => (
235+
<View>
236+
<View foo={foo} />
237+
</View>
238+
);
239+
240+
await act(() => ReactFabric.render(<Component foo={true} />, 11));
241+
expect(nativeFabricUIManager.completeRoot).toBeCalled();
242+
jest.clearAllMocks();
243+
244+
await act(() => ReactFabric.render(<Component foo={false} />, 11));
245+
expect(nativeFabricUIManager.cloneNode).not.toBeCalled();
246+
expect(nativeFabricUIManager.cloneNodeWithNewProps).toHaveBeenCalledTimes(
247+
1,
248+
);
249+
expect(nativeFabricUIManager.cloneNodeWithNewProps).toHaveBeenCalledWith(
250+
expect.anything(),
251+
{foo: false},
252+
);
253+
254+
expect(
255+
nativeFabricUIManager.cloneNodeWithNewChildren,
256+
).toHaveBeenCalledTimes(1);
257+
if (gate(flags => flags.passChildrenWhenCloningPersistedNodes)) {
258+
expect(
259+
nativeFabricUIManager.cloneNodeWithNewChildren,
260+
).toHaveBeenCalledWith(expect.anything(), [
261+
expect.objectContaining({props: {foo: false}}),
262+
]);
263+
expect(nativeFabricUIManager.appendChild).not.toBeCalled();
264+
} else {
265+
expect(
266+
nativeFabricUIManager.cloneNodeWithNewChildren,
267+
).toHaveBeenCalledWith(expect.anything());
268+
expect(nativeFabricUIManager.appendChild).toHaveBeenCalledTimes(1);
269+
}
270+
expect(
271+
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
272+
).not.toBeCalled();
273+
expect(nativeFabricUIManager.completeRoot).toBeCalled();
274+
});
275+
223276
it('should call dispatchCommand for native refs', async () => {
224277
const View = createReactNativeComponentClass('RCTView', () => ({
225278
validAttributes: {foo: true},

packages/react-noop-renderer/src/createReactNoop.js

+4-17
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
223223
type: string,
224224
oldProps: Props,
225225
newProps: Props,
226-
internalInstanceHandle: Object,
227226
keepChildren: boolean,
228-
recyclableInstance: null | Instance,
227+
children: ?$ReadOnlyArray<Instance>,
229228
): Instance {
230229
if (__DEV__) {
231230
checkPropStringCoercion(newProps.children, 'children');
@@ -234,7 +233,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
234233
id: instance.id,
235234
type: type,
236235
parent: instance.parent,
237-
children: keepChildren ? instance.children : [],
236+
children: keepChildren ? instance.children : children ?? [],
238237
text: shouldSetTextContent(type, newProps)
239238
? computeText((newProps.children: any) + '', instance.context)
240239
: null,
@@ -704,9 +703,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
704703
cloneInstance,
705704
clearContainer,
706705

707-
createContainerChildSet(
708-
container: Container,
709-
): Array<Instance | TextInstance> {
706+
createContainerChildSet(): Array<Instance | TextInstance> {
710707
return [];
711708
},
712709

@@ -742,25 +739,15 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
742739
instance: Instance,
743740
type: string,
744741
props: Props,
745-
internalInstanceHandle: Object,
746742
): Instance {
747-
const clone = cloneInstance(
748-
instance,
749-
type,
750-
props,
751-
props,
752-
internalInstanceHandle,
753-
true,
754-
null,
755-
);
743+
const clone = cloneInstance(instance, type, props, props, true, null);
756744
clone.hidden = true;
757745
return clone;
758746
},
759747

760748
cloneHiddenTextInstance(
761749
instance: TextInstance,
762750
text: string,
763-
internalInstanceHandle: Object,
764751
): TextInstance {
765752
const clone = {
766753
text: instance.text,

packages/react-reconciler/src/ReactFiberCommitWork.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1724,7 +1724,7 @@ function emptyPortalContainer(current: Fiber) {
17241724
...
17251725
} = current.stateNode;
17261726
const {containerInfo} = portal;
1727-
const emptyChildSet = createContainerChildSet(containerInfo);
1727+
const emptyChildSet = createContainerChildSet();
17281728
replaceContainerChildren(containerInfo, emptyChildSet);
17291729
}
17301730

0 commit comments

Comments
 (0)