From 6a04a2f64ac7db3bb3c22a341aefa049e989e09e Mon Sep 17 00:00:00 2001 From: Wolfgang Faust Date: Fri, 13 Feb 2026 09:27:56 -0800 Subject: [PATCH 1/2] add failing test for ItemSeparatorComponent state bug --- .../__tests__/VirtualizedSectionList-test.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js index cc6b1da8d266..068c4560ef09 100644 --- a/packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js +++ b/packages/virtualized-lists/Lists/__tests__/VirtualizedSectionList-test.js @@ -202,6 +202,64 @@ describe('VirtualizedSectionList', () => { expect(component).toMatchSnapshot(); }); + it('syncs ItemWithSeparator separator props when list re-renders with new leadingItem/trailingItem', async () => { + // Reproduces: When ItemWithSeparator re-renders with new props (e.g. after reorder), + // leadingItem/trailingItem can become stale (e.g. undefined when the row was previously at a boundary). + const separatorPropsReceived = []; + const ItemSeparatorWithCapture = props => { + separatorPropsReceived.push({ + leadingItem: props.leadingItem?.key, + trailingItem: props.trailingItem?.key, + }); + return ; + }; + + let setSections; + const initialSections = [ + {title: 's0', data: [{key: 'a'}, {key: 'b'}, {key: 'c'}]}, + ]; + const reorderedSections = [ + {title: 's0', data: [{key: 'b'}, {key: 'a'}, {key: 'c'}]}, + ]; + + function ListWithState() { + const [sections, setSectionsState] = React.useState(initialSections); + setSections = setSectionsState; + return ( + } + getItem={(data, index) => data[index]} + getItemCount={data => data.length} + keyExtractor={(item, index) => item.key} + /> + ); + } + + let component; + await ReactTestRenderer.act(() => { + component = ReactTestRenderer.create(); + }); + + separatorPropsReceived.length = 0; + + await ReactTestRenderer.act(() => { + setSections(reorderedSections); + }); + + // After reorder: row "b" moved from index 1 to 0. Its trailing separator + // (between b and a) should show leadingItem: b, trailingItem: a. + // But buggy ItemWithSeparator keeps the initial + // state from when "b" was at index 1 (leadingItem: a, trailingItem: c), so + // the separator receives stale props and this assertion fails. + expect(separatorPropsReceived).toEqual( + expect.arrayContaining([ + {leadingItem: 'b', trailingItem: 'a'}, + ]), + ); + }); + describe('scrollToLocation', () => { const ITEM_HEIGHT = 100; From 3ab0d4cd34b60dfbf209a9f561216cb5cb1c4e47 Mon Sep 17 00:00:00 2001 From: Wolfgang Faust Date: Mon, 23 Feb 2026 15:18:15 -0800 Subject: [PATCH 2/2] Add RNTester Playground reproducer for ItemSeparatorComponent state bug Co-authored-by: Cursor --- .../examples/Playground/RNTesterPlayground.js | 92 ++++++++++++++++++- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js index d37d0d4a9154..19990d64c08a 100644 --- a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js +++ b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js @@ -12,27 +12,111 @@ import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import RNTesterText from '../../components/RNTesterText'; import * as React from 'react'; -import {StyleSheet, View} from 'react-native'; +import {useState} from 'react'; +import { + Button, + SectionList, + StyleSheet, + Text, + View, +} from 'react-native'; + +const INITIAL_SECTIONS = [ + {title: 'Section', data: [{key: 'a'}, {key: 'b'}, {key: 'c'}]}, +]; +const REORDERED_SECTIONS = [ + {title: 'Section', data: [{key: 'b'}, {key: 'a'}, {key: 'c'}]}, +]; + +function ItemSeparatorReproducer({leadingItem, trailingItem}: any) { + const leading = leadingItem?.key ?? 'undefined'; + const trailing = trailingItem?.key ?? 'undefined'; + return ( + + + leading: {leading} | trailing: {trailing} + + + ); +} + +function ItemSeparatorComponentBugPlayground() { + const [sections, setSections] = useState(INITIAL_SECTIONS); + const [reordered, setReordered] = useState(false); -function Playground() { return ( - Edit "RNTesterPlayground.js" to change this file + Bug: ItemSeparatorComponent receives stale leadingItem/trailingItem after + reorder. Tap "Reorder" to swap first two items. The separator between + "b" and "a" should show "leading: b | trailing: a". If it shows + "leading: a | trailing: c" or "undefined", the bug is present. +