diff --git a/src/TabBar.tsx b/src/TabBar.tsx index 9025bd48..5a8aa4bc 100644 --- a/src/TabBar.tsx +++ b/src/TabBar.tsx @@ -11,6 +11,7 @@ import { Platform, FlatList, ListRenderItemInfo, + ViewToken, } from 'react-native'; import TabBarItem, { Props as TabBarItemProps } from './TabBarItem'; import TabBarIndicator, { Props as IndicatorProps } from './TabBarIndicator'; @@ -23,6 +24,7 @@ import type { Event, } from './types'; import useAnimatedValue from './useAnimatedValue'; +import useLatestCallback from 'use-latest-callback'; export type Props = SceneRendererProps & { navigationState: NavigationState; @@ -247,6 +249,10 @@ const renderIndicatorDefault = (props: IndicatorProps) => ( const getTestIdDefault = ({ route }: Scene) => route.testID; +// How many items measurements should we update per batch. +// Defaults to 10, since that's whats FlatList is using in initialNumToRender. +const MEASURE_PER_BATCH = 10; + export default function TabBar({ getLabelText = getLabelTextDefault, getAccessible = getAccessibleDefault, @@ -279,7 +285,7 @@ export default function TabBar({ }: Props) { const [layout, setLayout] = React.useState({ width: 0, height: 0 }); const [tabWidths, setTabWidths] = React.useState>({}); - const flatListRef = React.useRef(null); + const flatListRef = React.useRef(null); const isFirst = React.useRef(true); const scrollAmount = useAnimatedValue(0); const measuredTabWidths = React.useRef>({}); @@ -296,28 +302,19 @@ export default function TabBar({ flattenedTabWidth, }); - const hasMeasuredTabWidths = - Boolean(layout.width) && - routes.every((r) => typeof tabWidths[r.key] === 'number'); - React.useEffect(() => { if (isFirst.current) { isFirst.current = false; return; } - if (isWidthDynamic && !hasMeasuredTabWidths) { - // When tab width is dynamic, only adjust the scroll once we have all tab widths and layout - return; - } - if (scrollEnabled) { flatListRef.current?.scrollToOffset({ offset: scrollOffset, animated: true, }); } - }, [hasMeasuredTabWidths, isWidthDynamic, scrollEnabled, scrollOffset]); + }, [scrollEnabled, scrollOffset]); const handleLayout = (e: LayoutChangeEvent) => { const { height, width } = e.nativeEvent.layout; @@ -373,13 +370,24 @@ export default function TabBar({ ? (e: LayoutChangeEvent) => { measuredTabWidths.current[route.key] = e.nativeEvent.layout.width; - // When we have measured widths for all of the tabs, we should updates the state - // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app + // If we have more than 10 routes divide updating tabWidths into multiple batches. Here we update only first batch of 10 items. if ( + routes.length > MEASURE_PER_BATCH && + index === MEASURE_PER_BATCH && + routes + .slice(0, MEASURE_PER_BATCH) + .every( + (r) => typeof measuredTabWidths.current[r.key] === 'number' + ) + ) { + setTabWidths({ ...measuredTabWidths.current }); + } else if ( routes.every( (r) => typeof measuredTabWidths.current[r.key] === 'number' ) ) { + // When we have measured widths for all of the tabs, we should updates the state + // We avoid doing separate setState for each layout since it triggers multiple renders and slows down app setTabWidths({ ...measuredTabWidths.current }); } } @@ -494,6 +502,25 @@ export default function TabBar({ [scrollAmount] ); + const handleViewableItemsChanged = useLatestCallback( + ({ changed }: { changed: ViewToken[] }) => { + if (routes.length <= MEASURE_PER_BATCH) { + return; + } + // Get next vievable item + const item = changed[changed.length - 1]; + const index = item?.index || 0; + if ( + item.isViewable && + (index % 10 === 0 || + index === navigationState.index || + index === routes.length - 1) + ) { + setTabWidths({ ...measuredTabWidths.current }); + } + } + ); + return ( ({ data={routes as Animated.WithAnimatedValue[]} keyExtractor={keyExtractor} horizontal + initialNumToRender={MEASURE_PER_BATCH} accessibilityRole="tablist" keyboardShouldPersistTaps="handled" scrollEnabled={scrollEnabled} @@ -549,6 +577,7 @@ export default function TabBar({ scrollEventThrottle={16} renderItem={renderItem} onScroll={handleScroll} + onViewableItemsChanged={handleViewableItemsChanged} ref={flatListRef} testID={testID} /> diff --git a/src/TabBarIndicator.tsx b/src/TabBarIndicator.tsx index 8076d300..583f4080 100644 --- a/src/TabBarIndicator.tsx +++ b/src/TabBarIndicator.tsx @@ -59,9 +59,12 @@ export default function TabBarIndicator({ const opacity = useAnimatedValue(isWidthDynamic ? 0 : 1); - const hasMeasuredTabWidths = - Boolean(layout.width) && - navigationState.routes.every((_, i) => getTabWidth(i)); + const indicatorVisible = isWidthDynamic + ? layout.width && + navigationState.routes + .slice(0, navigationState.index) + .every((_, r) => getTabWidth(r)) + : true; React.useEffect(() => { const fadeInIndicator = () => { @@ -69,7 +72,7 @@ export default function TabBarIndicator({ !isIndicatorShown.current && isWidthDynamic && // We should fade-in the indicator when we have widths for all the tab items - hasMeasuredTabWidths + indicatorVisible ) { isIndicatorShown.current = true; @@ -85,7 +88,7 @@ export default function TabBarIndicator({ fadeInIndicator(); return () => opacity.stopAnimation(); - }, [hasMeasuredTabWidths, isWidthDynamic, opacity]); + }, [indicatorVisible, isWidthDynamic, opacity]); const { routes } = navigationState;