Skip to content

Commit f14d1f9

Browse files
antonisclaude
andcommitted
fix(core): Prevent text duplication from props.children and HostText child fibers
In real React Native fiber trees, <Text>Hello</Text> has both memoizedProps.children = 'Hello' on the Text fiber and a child HostText fiber with memoizedProps = 'Hello'. Skip child recursion when props.children is a string to avoid collecting the same text twice. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 92517ae commit f14d1f9

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

packages/core/src/js/touchevents.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,12 +484,16 @@ function collectTextFromFiber(
484484

485485
const props = inst.memoizedProps;
486486
if (typeof props === 'string') {
487+
// Raw text fiber (HostText) — no children to recurse into
487488
parts.push(props);
488489
} else if (typeof props?.children === 'string') {
490+
// Component with string children — skip child recursion to avoid
491+
// duplicating text from the HostText child fiber
489492
parts.push(props.children);
493+
} else {
494+
collectTextFromFiber(inst.child, parts, depth + 1, 0);
490495
}
491496

492-
collectTextFromFiber(inst.child, parts, depth + 1, 0);
493497
collectTextFromFiber(inst.sibling, parts, depth, siblingIndex + 1);
494498
}
495499

packages/core/test/touchevents.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,42 @@ describe('TouchEventBoundary._onTouchStart', () => {
934934
);
935935
});
936936

937+
it('does not duplicate text when props.children and HostText child both exist', () => {
938+
// In real React Native fiber trees, <Text>Hello</Text> has both:
939+
// - Text fiber: memoizedProps = { children: 'Hello' }
940+
// - HostText child fiber: memoizedProps = 'Hello' (raw string)
941+
const { defaultProps } = TouchEventBoundary;
942+
const boundary = new TouchEventBoundary(defaultProps);
943+
jest.spyOn(client, 'getIntegrationByName').mockReturnValue(undefined);
944+
945+
const event = {
946+
_targetInst: {
947+
elementType: { displayName: 'TouchableOpacity' },
948+
memoizedProps: {},
949+
child: {
950+
elementType: { name: 'Text' },
951+
memoizedProps: { children: 'Save workout' },
952+
child: {
953+
// HostText fiber — raw string props
954+
memoizedProps: 'Save workout',
955+
},
956+
},
957+
},
958+
};
959+
960+
// @ts-expect-error Calling private member
961+
boundary._onTouchStart(event);
962+
963+
expect(addBreadcrumb).toHaveBeenCalledWith(
964+
expect.objectContaining({
965+
message: 'Touch event within element: Save workout',
966+
data: {
967+
path: [{ name: 'TouchableOpacity', label: 'Save workout' }],
968+
},
969+
}),
970+
);
971+
});
972+
937973
it('extracts text from nested fiber children', () => {
938974
const { defaultProps } = TouchEventBoundary;
939975
const boundary = new TouchEventBoundary(defaultProps);

0 commit comments

Comments
 (0)