Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 88 additions & 4 deletions packages/rn-tester/js/examples/Playground/RNTesterPlayground.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<View style={styles.separator}>
<Text style={styles.separatorText}>
leading: {leading} | trailing: {trailing}
</Text>
</View>
);
}

function ItemSeparatorComponentBugPlayground() {
const [sections, setSections] = useState(INITIAL_SECTIONS);
const [reordered, setReordered] = useState(false);

function Playground() {
return (
<View style={styles.container}>
<RNTesterText>
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.
</RNTesterText>
<Button
title={reordered ? 'Reset' : 'Reorder (swap first two)'}
onPress={() => {
setReordered(r => !r);
setSections(reordered ? INITIAL_SECTIONS : REORDERED_SECTIONS);
}}
/>
<SectionList
style={styles.list}
sections={sections}
keyExtractor={item => item.key}
renderItem={({item}) => (
<View style={styles.item}>
<Text>{item.key}</Text>
</View>
)}
renderSectionHeader={({section: {title}}) => (
<Text style={styles.sectionHeader}>{title}</Text>
)}
ItemSeparatorComponent={ItemSeparatorReproducer}
/>
</View>
);
}

function Playground() {
return (
<View style={styles.container}>
<ItemSeparatorComponentBugPlayground />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
item: {
padding: 12,
backgroundColor: '#f0f0f0',
},
list: {
flex: 1,
},
sectionHeader: {
fontSize: 16,
fontWeight: 'bold',
paddingVertical: 8,
},
separator: {
height: 24,
backgroundColor: '#e0e0ff',
justifyContent: 'center',
paddingHorizontal: 8,
},
separatorText: {
fontSize: 12,
},
});

export default ({
title: 'Playground',
name: 'playground',
description: 'Test out new features and ideas.',
description:
'Test out new features and ideas. Includes reproducer for ItemSeparatorComponent leadingItem/trailingItem state bug.',
render: (): React.Node => <Playground />,
}: RNTesterModuleExample);
Original file line number Diff line number Diff line change
Expand Up @@ -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 <separator {...props} />;
};

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 (
<VirtualizedSectionList
ItemSeparatorComponent={ItemSeparatorWithCapture}
sections={sections}
renderItem={({item}) => <item title={item.key} />}
getItem={(data, index) => data[index]}
getItemCount={data => data.length}
keyExtractor={(item, index) => item.key}
/>
);
}

let component;
await ReactTestRenderer.act(() => {
component = ReactTestRenderer.create(<ListWithState />);
});

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;

Expand Down
Loading