Skip to content

Commit c183a8f

Browse files
hryhoriiK97claude
andcommitted
Keep skipping unchanged children when a leaf without a view is replaced
With skipUnchangedChildrenInDifferentiator on, the side-by-side slicing gives up when a child's tag changes. React gives a Text a new raw text node whenever its string changes, and on Android a Paragraph doesn't form a stacking context, so the differ reaches that node while slicing the Paragraph's parent and slices all of the parent's children in full. A replaced leaf that doesn't form a view has no mount index, so both nodes can be listed as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 57cc022 commit c183a8f

2 files changed

Lines changed: 80 additions & 2 deletions

File tree

‎packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,39 @@ bool sliceChangedChildShadowNodeViewPairsRecursively(
267267
if (oldChildShadowNode.getTag() != newChildShadowNode.getTag() ||
268268
oldChildShadowNode.getOrderIndex() !=
269269
newChildShadowNode.getOrderIndex()) {
270-
return false;
270+
// A replaced leaf that doesn't form a view (like the raw text of a
271+
// `Text` whose string changed) has no mount index, so both nodes can be
272+
// listed as they are without shifting the views around them.
273+
if (!oldChildShadowNode.getChildren().empty() ||
274+
!newChildShadowNode.getChildren().empty()) {
275+
return false;
276+
}
277+
auto oldLeafSlice = sliceChild(
278+
oldShadowNode,
279+
oldChildShadowNode,
280+
ShadowView(oldChildShadowNode),
281+
oldLayoutOffset);
282+
auto newLeafSlice = sliceChild(
283+
newShadowNode,
284+
newChildShadowNode,
285+
ShadowView(newChildShadowNode),
286+
newLayoutOffset);
287+
if (oldLeafSlice.isConcreteView || newLeafSlice.isConcreteView) {
288+
return false;
289+
}
290+
addChildSlice(
291+
oldPairList,
292+
oldStartOfStaticIndex,
293+
scope,
294+
oldLeafSlice,
295+
oldChildShadowNode);
296+
addChildSlice(
297+
newPairList,
298+
newStartOfStaticIndex,
299+
scope,
300+
newLeafSlice,
301+
newChildShadowNode);
302+
continue;
271303
}
272304

273305
auto oldChildSlice = sliceChild(

‎packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type {HostInstance} from 'react-native';
1717
import * as Fantom from '@react-native/fantom';
1818
import nullthrows from 'nullthrows';
1919
import * as React from 'react';
20-
import {ScrollView, View} from 'react-native';
20+
import {ScrollView, Text, View} from 'react-native';
2121

2222
describe('ViewFlattening', () => {
2323
/**
@@ -593,6 +593,52 @@ describe('unchanged children', () => {
593593
]);
594594
});
595595

596+
test('a text whose string changes next to unchanged children is updated', () => {
597+
const root = Fantom.createRoot();
598+
const render = (value: number) =>
599+
Fantom.runTask(() => {
600+
root.render(
601+
<View nativeID="parent">
602+
<Unchanged nativeID="A" />
603+
<Text nativeID="label">Value {value}</Text>
604+
<Unchanged nativeID="B" />
605+
</View>,
606+
);
607+
});
608+
609+
render(1);
610+
root.takeMountingManagerLogs();
611+
612+
// React gives the changed string a new raw text node, which doesn't form
613+
// a view, so only the paragraph is updated.
614+
render(2);
615+
expect(root.takeMountingManagerLogs()).toEqual([
616+
'Update {type: "Paragraph", nativeID: "label"}',
617+
]);
618+
});
619+
620+
test('an empty view that is remounted next to unchanged children causes no mutations', () => {
621+
const root = Fantom.createRoot();
622+
const render = (version: number) =>
623+
Fantom.runTask(() => {
624+
root.render(
625+
<View nativeID="parent">
626+
<Unchanged nativeID="A" />
627+
<View key={version} />
628+
<Unchanged nativeID="B" />
629+
</View>,
630+
);
631+
});
632+
633+
render(1);
634+
root.takeMountingManagerLogs();
635+
636+
// The new key gives the empty view a new tag. It is flattened and has no
637+
// children, so it doesn't form a view and nothing is mounted for it.
638+
render(2);
639+
expect(root.takeMountingManagerLogs()).toEqual([]);
640+
});
641+
596642
test('a child added next to unchanged children is inserted', () => {
597643
const root = Fantom.createRoot();
598644
const render = (showAdded: boolean) =>

0 commit comments

Comments
 (0)