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

Commit a25674f

Browse files
committed
feat: split updating state to batches on long lists
1 parent cafe21c commit a25674f

File tree

2 files changed

+50
-18
lines changed

2 files changed

+50
-18
lines changed

src/TabBar.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Platform,
1212
FlatList,
1313
ListRenderItemInfo,
14+
ViewToken,
1415
} from 'react-native';
1516
import TabBarItem, { Props as TabBarItemProps } from './TabBarItem';
1617
import TabBarIndicator, { Props as IndicatorProps } from './TabBarIndicator';
@@ -23,6 +24,7 @@ import type {
2324
Event,
2425
} from './types';
2526
import useAnimatedValue from './useAnimatedValue';
27+
import useLatestCallback from 'use-latest-callback';
2628

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

248250
const getTestIdDefault = ({ route }: Scene<Route>) => route.testID;
249251

252+
// How many items measurements should we update per batch.
253+
// Defaults to 10, since that's whats FlatList is using in initialNumToRender.
254+
const MEASURE_PER_BATCH = 10;
255+
250256
export default function TabBar<T extends Route>({
251257
getLabelText = getLabelTextDefault,
252258
getAccessible = getAccessibleDefault,
@@ -279,7 +285,7 @@ export default function TabBar<T extends Route>({
279285
}: Props<T>) {
280286
const [layout, setLayout] = React.useState<Layout>({ width: 0, height: 0 });
281287
const [tabWidths, setTabWidths] = React.useState<Record<string, number>>({});
282-
const flatListRef = React.useRef<FlatList>(null);
288+
const flatListRef = React.useRef<FlatList | null>(null);
283289
const isFirst = React.useRef(true);
284290
const scrollAmount = useAnimatedValue(0);
285291
const measuredTabWidths = React.useRef<Record<string, number>>({});
@@ -296,28 +302,19 @@ export default function TabBar<T extends Route>({
296302
flattenedTabWidth,
297303
});
298304

299-
const hasMeasuredTabWidths =
300-
Boolean(layout.width) &&
301-
routes.every((r) => typeof tabWidths[r.key] === 'number');
302-
303305
React.useEffect(() => {
304306
if (isFirst.current) {
305307
isFirst.current = false;
306308
return;
307309
}
308310

309-
if (isWidthDynamic && !hasMeasuredTabWidths) {
310-
// When tab width is dynamic, only adjust the scroll once we have all tab widths and layout
311-
return;
312-
}
313-
314311
if (scrollEnabled) {
315312
flatListRef.current?.scrollToOffset({
316313
offset: scrollOffset,
317314
animated: true,
318315
});
319316
}
320-
}, [hasMeasuredTabWidths, isWidthDynamic, scrollEnabled, scrollOffset]);
317+
}, [scrollEnabled, scrollOffset]);
321318

322319
const handleLayout = (e: LayoutChangeEvent) => {
323320
const { height, width } = e.nativeEvent.layout;
@@ -373,13 +370,24 @@ export default function TabBar<T extends Route>({
373370
? (e: LayoutChangeEvent) => {
374371
measuredTabWidths.current[route.key] = e.nativeEvent.layout.width;
375372

376-
// When we have measured widths for all of the tabs, we should updates the state
377-
// We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
373+
// If we have more than 10 routes divide updating tabWidths into multiple batches. Here we update only first batch of 10 items.
378374
if (
375+
routes.length > MEASURE_PER_BATCH &&
376+
index === MEASURE_PER_BATCH &&
377+
routes
378+
.slice(0, MEASURE_PER_BATCH)
379+
.every(
380+
(r) => typeof measuredTabWidths.current[r.key] === 'number'
381+
)
382+
) {
383+
setTabWidths({ ...measuredTabWidths.current });
384+
} else if (
379385
routes.every(
380386
(r) => typeof measuredTabWidths.current[r.key] === 'number'
381387
)
382388
) {
389+
// When we have measured widths for all of the tabs, we should updates the state
390+
// We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
383391
setTabWidths({ ...measuredTabWidths.current });
384392
}
385393
}
@@ -494,6 +502,25 @@ export default function TabBar<T extends Route>({
494502
[scrollAmount]
495503
);
496504

505+
const handleViewableItemsChanged = useLatestCallback(
506+
({ changed }: { changed: ViewToken[] }) => {
507+
if (routes.length <= MEASURE_PER_BATCH) {
508+
return;
509+
}
510+
// Get next vievable item
511+
const item = changed[changed.length - 1];
512+
const index = item?.index || 0;
513+
if (
514+
item.isViewable &&
515+
(index % 10 === 0 ||
516+
index === navigationState.index ||
517+
index === routes.length - 1)
518+
) {
519+
setTabWidths({ ...measuredTabWidths.current });
520+
}
521+
}
522+
);
523+
497524
return (
498525
<Animated.View onLayout={handleLayout} style={[styles.tabBar, style]}>
499526
<Animated.View
@@ -535,6 +562,7 @@ export default function TabBar<T extends Route>({
535562
data={routes as Animated.WithAnimatedValue<T>[]}
536563
keyExtractor={keyExtractor}
537564
horizontal
565+
initialNumToRender={MEASURE_PER_BATCH}
538566
accessibilityRole="tablist"
539567
keyboardShouldPersistTaps="handled"
540568
scrollEnabled={scrollEnabled}
@@ -549,6 +577,7 @@ export default function TabBar<T extends Route>({
549577
scrollEventThrottle={16}
550578
renderItem={renderItem}
551579
onScroll={handleScroll}
580+
onViewableItemsChanged={handleViewableItemsChanged}
552581
ref={flatListRef}
553582
testID={testID}
554583
/>

src/TabBarIndicator.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,20 @@ export default function TabBarIndicator<T extends Route>({
5959

6060
const opacity = useAnimatedValue(isWidthDynamic ? 0 : 1);
6161

62-
const hasMeasuredTabWidths =
63-
Boolean(layout.width) &&
64-
navigationState.routes.every((_, i) => getTabWidth(i));
62+
const indicatorVisible = isWidthDynamic
63+
? layout.width &&
64+
navigationState.routes
65+
.slice(0, navigationState.index)
66+
.every((_, r) => getTabWidth(r))
67+
: true;
6568

6669
React.useEffect(() => {
6770
const fadeInIndicator = () => {
6871
if (
6972
!isIndicatorShown.current &&
7073
isWidthDynamic &&
7174
// We should fade-in the indicator when we have widths for all the tab items
72-
hasMeasuredTabWidths
75+
indicatorVisible
7376
) {
7477
isIndicatorShown.current = true;
7578

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

8790
return () => opacity.stopAnimation();
88-
}, [hasMeasuredTabWidths, isWidthDynamic, opacity]);
91+
}, [indicatorVisible, isWidthDynamic, opacity]);
8992

9093
const { routes } = navigationState;
9194

0 commit comments

Comments
 (0)