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 35d13cb
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 10 deletions.
52 changes: 48 additions & 4 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 Down Expand Up @@ -247,6 +248,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,8 +284,9 @@ 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 howManyMeasured = React.useRef(0);
const scrollAmount = useAnimatedValue(0);
const measuredTabWidths = React.useRef<Record<string, number>>({});

Expand All @@ -298,7 +304,14 @@ export default function TabBar<T extends Route>({

const hasMeasuredTabWidths =
Boolean(layout.width) &&
routes.every((r) => typeof tabWidths[r.key] === 'number');
routes
.slice(
0,
routes.length > MEASURE_PER_BATCH
? howManyMeasured.current
: routes.length
)
.every((r) => typeof tabWidths[r.key] === 'number');

React.useEffect(() => {
if (isFirst.current) {
Expand Down Expand Up @@ -373,13 +386,25 @@ 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 });
howManyMeasured.current = MEASURE_PER_BATCH;
} 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 +519,22 @@ export default function TabBar<T extends Route>({
[scrollAmount]
);

const handleViewableItemsChanged = React.useCallback(
({ changed }: { changed: ViewToken[] }) => {
if (routes.length <= MEASURE_PER_BATCH) {
return;
}
// Get next vievable item
const [item] = changed;
const index = item.index || 0;
if (item.isViewable && index >= howManyMeasured.current) {
setTabWidths({ ...measuredTabWidths.current });
howManyMeasured.current += MEASURE_PER_BATCH;
}
},
[routes.length]
);

return (
<Animated.View onLayout={handleLayout} style={[styles.tabBar, style]}>
<Animated.View
Expand All @@ -513,6 +554,7 @@ export default function TabBar<T extends Route>({
position,
layout,
navigationState,
visible: hasMeasuredTabWidths,
jumpTo,
width: isWidthDynamic
? 'auto'
Expand All @@ -535,6 +577,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 +592,7 @@ export default function TabBar<T extends Route>({
scrollEventThrottle={16}
renderItem={renderItem}
onScroll={handleScroll}
onViewableItemsChanged={handleViewableItemsChanged}
ref={flatListRef}
testID={testID}
/>
Expand Down
10 changes: 4 additions & 6 deletions src/TabBarIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type Props<T extends Route> = SceneRendererProps & {
navigationState: NavigationState<T>;
width: string | number;
style?: StyleProp<ViewStyle>;
visible?: boolean;
getTabWidth: GetTabWidth;
gap?: number;
};
Expand Down Expand Up @@ -51,6 +52,7 @@ export default function TabBarIndicator<T extends Route>({
navigationState,
position,
width,
visible,
gap,
style,
}: Props<T>) {
Expand All @@ -59,17 +61,13 @@ export default function TabBarIndicator<T extends Route>({

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

const hasMeasuredTabWidths =
Boolean(layout.width) &&
navigationState.routes.every((_, i) => getTabWidth(i));

React.useEffect(() => {
const fadeInIndicator = () => {
if (
!isIndicatorShown.current &&
isWidthDynamic &&
// We should fade-in the indicator when we have widths for all the tab items
hasMeasuredTabWidths
visible
) {
isIndicatorShown.current = true;

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

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

const { routes } = navigationState;

Expand Down

0 comments on commit 35d13cb

Please sign in to comment.