Skip to content
This repository has been archived by the owner on Nov 27, 2022. It is now read-only.

feat: split updating state to batches on long lists #1409

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
55 changes: 42 additions & 13 deletions src/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,6 +24,7 @@ import type {
Event,
} from './types';
import useAnimatedValue from './useAnimatedValue';
import useLatestCallback from 'use-latest-callback';

export type Props<T extends Route> = SceneRendererProps & {
navigationState: NavigationState<T>;
Expand Down Expand Up @@ -247,6 +249,10 @@ const renderIndicatorDefault = (props: IndicatorProps<Route>) => (

const getTestIdDefault = ({ route }: Scene<Route>) => 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<T extends Route>({
getLabelText = getLabelTextDefault,
getAccessible = getAccessibleDefault,
Expand Down Expand Up @@ -279,7 +285,7 @@ export default function TabBar<T extends Route>({
}: Props<T>) {
const [layout, setLayout] = React.useState<Layout>({ width: 0, height: 0 });
const [tabWidths, setTabWidths] = React.useState<Record<string, number>>({});
const flatListRef = React.useRef<FlatList>(null);
const flatListRef = React.useRef<FlatList | null>(null);
const isFirst = React.useRef(true);
const scrollAmount = useAnimatedValue(0);
const measuredTabWidths = React.useRef<Record<string, number>>({});
Expand All @@ -296,28 +302,19 @@ export default function TabBar<T extends Route>({
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;
Expand Down Expand Up @@ -373,13 +370,24 @@ export default function TabBar<T extends Route>({
? (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 });
}
}
Expand Down Expand Up @@ -494,6 +502,25 @@ export default function TabBar<T extends Route>({
[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 (
<Animated.View onLayout={handleLayout} style={[styles.tabBar, style]}>
<Animated.View
Expand Down Expand Up @@ -535,6 +562,7 @@ export default function TabBar<T extends Route>({
data={routes as Animated.WithAnimatedValue<T>[]}
keyExtractor={keyExtractor}
horizontal
initialNumToRender={MEASURE_PER_BATCH}
accessibilityRole="tablist"
keyboardShouldPersistTaps="handled"
scrollEnabled={scrollEnabled}
Expand All @@ -549,6 +577,7 @@ export default function TabBar<T extends Route>({
scrollEventThrottle={16}
renderItem={renderItem}
onScroll={handleScroll}
onViewableItemsChanged={handleViewableItemsChanged}
ref={flatListRef}
testID={testID}
/>
Expand Down
13 changes: 8 additions & 5 deletions src/TabBarIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,20 @@ export default function TabBarIndicator<T extends Route>({

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 = () => {
if (
!isIndicatorShown.current &&
isWidthDynamic &&
// We should fade-in the indicator when we have widths for all the tab items
hasMeasuredTabWidths
indicatorVisible
) {
isIndicatorShown.current = true;

Expand All @@ -85,7 +88,7 @@ export default function TabBarIndicator<T extends Route>({
fadeInIndicator();

return () => opacity.stopAnimation();
}, [hasMeasuredTabWidths, isWidthDynamic, opacity]);
}, [indicatorVisible, isWidthDynamic, opacity]);

const { routes } = navigationState;

Expand Down