Skip cloning Fragments in ListEmptyComponent to avoid onLayout warning (#50833)

Summary:
Fixes https://github.com/facebook/react-native/issues/50817

Using a fragment is very common when rendering elements. We are cloning and adding `onLayout` always to the ListEmptyComponent element, but this would seem to work only when `View` is used for this as a wrapper in this prop. To prevent this unnecessary warning, I think we can easily check whether it is a fragment or not before cloning and adding the extra props – this adds backwards compatibility for those that don't need to use `onLayout`.

## Changelog:

[GENERAL] [FIXED] - Skip cloning Fragments in ListEmptyComponent to avoid onLayout warning

Pull Request resolved: https://github.com/facebook/react-native/pull/50833

Test Plan: Use the code snippet from the linked issue to verify that the warning is not thrown anymore when using a Fragment.

Reviewed By: javache

Differential Revision: D73421503

Pulled By: rshest

fbshipit-source-id: 0da4a38130601943e4704589ac275eba39767191
This commit is contained in:
Mateo Guzmán
2025-04-22 07:06:07 -07:00
committed by Facebook GitHub Bot
parent 04348e9db6
commit 2b0189b964
2 changed files with 114 additions and 11 deletions
@@ -891,6 +891,30 @@ class VirtualizedList extends StateSafePureComponent<
return key;
}
_renderEmptyComponent(
element: ExactReactElement_DEPRECATED<any>,
inversionStyle: StyleProp<ViewStyle>,
): React.Node {
// $FlowFixMe[prop-missing] React.Element internal inspection
const isFragment = element.type === React.Fragment;
if (isFragment) {
return element;
}
return React.cloneElement(element, {
onLayout: (event: LayoutChangeEvent) => {
this._onLayoutEmpty(event);
// $FlowFixMe[prop-missing] React.Element internal inspection
if (element.props.onLayout) {
element.props.onLayout(event);
}
},
// $FlowFixMe[prop-missing] React.Element internal inspection
style: StyleSheet.compose(inversionStyle, element.props.style),
});
}
render(): React.Node {
this._checkProps(this.props);
const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} =
@@ -956,17 +980,7 @@ class VirtualizedList extends StateSafePureComponent<
<VirtualizedListCellContextProvider
cellKey={this._getCellKey() + '-empty'}
key="$empty">
{React.cloneElement(element, {
onLayout: (event: LayoutChangeEvent) => {
this._onLayoutEmpty(event);
// $FlowFixMe[prop-missing] React.Element internal inspection
if (element.props.onLayout) {
element.props.onLayout(event);
}
},
// $FlowFixMe[prop-missing] React.Element internal inspection
style: StyleSheet.compose(inversionStyle, element.props.style),
})}
{this._renderEmptyComponent(element, inversionStyle)}
</VirtualizedListCellContextProvider>,
);
}
@@ -337,6 +337,95 @@ describe('VirtualizedList', () => {
});
});
it('empty component returns the original element if it is a React.Fragment', () => {
const listRef = React.createRef();
const fragment = (
<React.Fragment>
<div>Test</div>
</React.Fragment>
);
act(() => {
create(
<VirtualizedList
ref={listRef}
data={[]}
ListEmptyComponent={fragment}
getItem={(data, index) => data[index]}
getItemCount={data => data.length}
renderItem={({item}) => <item value={item.key} />}
/>,
);
});
const result = listRef.current._renderEmptyComponent(fragment, null);
expect(result).toBe(fragment);
});
it('empty component clones the element and adds onLayout and style props if not a Fragment', () => {
const listRef = React.createRef();
const element = <div>Test</div>;
const inversionStyle = {transform: [{scaleY: -1}]};
act(() => {
create(
<VirtualizedList
ref={listRef}
data={[]}
ListEmptyComponent={element}
getItem={(data, index) => data[index]}
getItemCount={data => data.length}
renderItem={({item}) => <item value={item.key} />}
/>,
);
});
const result = listRef.current._renderEmptyComponent(
element,
inversionStyle,
);
// The result should be a cloned element with additional props
expect(result).not.toBe(element);
expect(result.props).toEqual(
expect.objectContaining({
onLayout: expect.any(Function),
style: inversionStyle,
}),
);
});
it('empty component preserves original onLayout handler if present', () => {
const listRef = React.createRef();
const originalOnLayout = jest.fn();
const element = <div onLayout={originalOnLayout}>Test</div>;
const inversionStyle = {transform: [{scaleY: -1}]};
act(() => {
create(
<VirtualizedList
ref={listRef}
data={[]}
ListEmptyComponent={element}
getItem={(data, index) => data[index]}
getItemCount={data => data.length}
renderItem={({item}) => <item value={item.key} />}
/>,
);
});
const result = listRef.current._renderEmptyComponent(
element,
inversionStyle,
);
// Call the onLayout handler
result.props.onLayout({nativeEvent: {layout: {width: 100, height: 100}}});
// Both the original and the new onLayout should be called
expect(originalOnLayout).toHaveBeenCalled();
});
it('returns the viewableItems correctly in the onViewableItemsChanged callback after changing the data', async () => {
const ITEM_HEIGHT = 800;
let data = [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}];