/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format */ 'use strict'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import type {ListRenderItemInfo} from 'react-native'; import type {ViewToken} from 'react-native/Libraries/Lists/ViewabilityHelper'; import RNTesterPage from '../../components/RNTesterPage'; import RNTesterText from '../../components/RNTesterText'; import * as React from 'react'; import {useCallback, useEffect, useMemo, useReducer} from 'react'; import {FlatList, StyleSheet, Text, View} from 'react-native'; type OuterItem = 'head' | 'vertical' | 'horizontal' | 'filler'; const outerItems: OuterItem[] = [ 'head', 'vertical', 'filler', 'horizontal', 'filler', 'vertical', ]; const items = [1, 2, 3, 4, 5]; type ItemsState = { renderedItems: number[], viewableItems: number[], }; const initialItemsState: ItemsState = { renderedItems: [], viewableItems: [], }; type ItemsAction = { type: 'add-rendered' | 'add-viewable' | 'remove-rendered' | 'remove-viewable', item: number, }; function reducer(state: ItemsState, action: ItemsAction): ItemsState { if (action.type === 'add-rendered') { if (state.renderedItems.includes(action.item)) { return state; } else { return {...state, renderedItems: [...state.renderedItems, action.item]}; } } else if (action.type === 'add-viewable') { if (state.viewableItems.includes(action.item)) { return state; } else { return {...state, viewableItems: [...state.viewableItems, action.item]}; } } else if (action.type === 'remove-rendered') { return { ...state, renderedItems: state.renderedItems.filter(i => i !== action.item), }; } else if (action.type === 'remove-viewable') { return { ...state, viewableItems: state.viewableItems.filter(i => i !== action.item), }; } return state; } function NestedListExample(): React.Node { const [outer, dispatchOuter] = useReducer(reducer, initialItemsState); const [inner, dispatchInner] = useReducer(reducer, initialItemsState); const sortedInnerViewableItems = useMemo( () => [...inner.viewableItems].sort((a, b) => a - b).join(', '), [inner.viewableItems], ); const sortedInnerRenderedItems = useMemo( () => [...inner.renderedItems].sort((a, b) => a - b).join(', '), [inner.renderedItems], ); const onViewableItemsChanged = useCallback( ({ changed, }: { changed: Array, viewableItems: Array, ... }) => { for (const token of changed) { dispatchOuter({ type: token.isViewable ? 'add-viewable' : 'remove-viewable', item: token.index ?? -1, }); } }, [dispatchOuter], ); return ( Outer Viewable:{'\n'} {outerItems .map((item, i) => ({item, i})) .filter(o => outer.viewableItems.includes(o.i)) .map(({item, i}) => `${i} (${item})`) .join(', ')} Outer Rendered:{'\n'} {outerItems .map((item, i) => ({item, i})) .filter(o => outer.renderedItems.includes(o.i)) .map(({item, i}) => `${i} (${item})`) .join(', ')} Inner Viewable:{'\n'} {sortedInnerViewableItems} Inner Rendered:{'\n'} {sortedInnerRenderedItems} ( )} style={styles.list} windowSize={3} initialNumToRender={1} onViewableItemsChanged={onViewableItemsChanged} /> ); } function OuterItemRenderer({ index, item, dispatchOuter, dispatchInner, }: { index: number, item: OuterItem, dispatchOuter: ItemsAction => void, dispatchInner: ItemsAction => void, ... }) { useEffect(() => { dispatchOuter({ type: 'add-rendered', item: index, }); return () => { dispatchOuter({ type: 'remove-rendered', item: index, }); }; }, [dispatchOuter, index]); const onViewableItemsChanged = useCallback( ({ changed, }: { changed: Array, viewableItems: Array, ... }) => { for (const token of changed) { dispatchInner({ type: token.isViewable ? 'add-viewable' : 'remove-viewable', item: token.item, }); } }, [dispatchInner], ); switch (item) { case 'head': return ( Header ); case 'vertical': return ( index * items.length * 3 + i)} renderItem={(p: ListRenderItemInfo) => ( )} style={styles.childList} onViewableItemsChanged={onViewableItemsChanged} windowSize={1} initialNumToRender={1} /> index * items.length * 3 + i + items.length)} renderItem={(p: ListRenderItemInfo) => ( )} style={styles.childList} onViewableItemsChanged={onViewableItemsChanged} windowSize={1} initialNumToRender={1} /> ); case 'horizontal': return ( index * items.length * 3 + i + 2 * items.length, )} renderItem={(p: ListRenderItemInfo) => ( )} style={styles.childList} onViewableItemsChanged={onViewableItemsChanged} /> ); case 'filler': return ; } } function InnerItemRenderer({ item, dispatchInner, }: { item: number, dispatchInner: ItemsAction => void, ... }) { useEffect(() => { dispatchInner({ type: 'add-rendered', item: item, }); return () => { dispatchInner({ type: 'remove-rendered', item: item, }); }; }, [dispatchInner, item]); return ( {item} ); } const styles = StyleSheet.create({ debugText: { fontSize: 10, }, debugTextHeader: { fontWeight: 'bold', }, list: { borderWidth: 1, borderColor: 'black', }, body: { flexDirection: 'row', justifyContent: 'space-around', }, col: { flex: 1, padding: 10, }, row: { flex: 1, }, filler: { height: 72, backgroundColor: 'lightblue', }, childList: { backgroundColor: 'lightgreen', }, header: { height: 40, backgroundColor: 'lightcoral', alignItems: 'center', justifyContent: 'center', }, cell: { padding: 10, }, item: { alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: 'black', backgroundColor: 'oldlace', height: 72, minWidth: 144, }, }); export default ({ title: 'Nested', description: 'Nested FlatLists of same and opposite orientation', name: 'nested', render: NestedListExample, }: RNTesterModuleExample);