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

Commit

Permalink
feat: split updating state to batches on long lists
Browse files Browse the repository at this point in the history
  • Loading branch information
okwasniewski committed Nov 16, 2022
1 parent cafe21c commit a25674f
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 18 deletions.
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

0 comments on commit a25674f

Please sign in to comment.