Skip to content

Commit

Permalink
fix: split updating state to batches on long lists (react-navigation#…
Browse files Browse the repository at this point in the history
…11046)

**Motivation**

As reported here:
satya164/react-native-tab-view#1405, when
using really long lists header wouldn't get all tab widths and therefore
would stop rendering. If we have more than 10 routes we split rendering
into multiple batches using onViewableItemsChanged.

**Test plan**


https://user-images.githubusercontent.com/52801365/204381404-47fe743c-e79a-45ba-9c05-08c8cdd9f92e.mp4

Launch Scrollable tab bar example and scroll.


The change must pass lint, typescript and tests.
  • Loading branch information
okwasniewski authored Nov 29, 2022
1 parent 7d6bd04 commit 6345fac
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 8 deletions.
47 changes: 44 additions & 3 deletions packages/react-native-tab-view/src/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
TextStyle,
View,
ViewStyle,
ViewToken,
} from 'react-native';
import useLatestCallback from 'use-latest-callback';

import TabBarIndicator, { Props as IndicatorProps } from './TabBarIndicator';
import TabBarItem, { Props as TabBarItemProps } from './TabBarItem';
Expand Down Expand Up @@ -248,6 +250,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 @@ -280,7 +286,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 @@ -299,7 +305,9 @@ export default function TabBar<T extends Route>({

const hasMeasuredTabWidths =
Boolean(layout.width) &&
routes.every((r) => typeof tabWidths[r.key] === 'number');
routes
.slice(0, navigationState.index)
.every((r) => typeof tabWidths[r.key] === 'number');

React.useEffect(() => {
if (isFirst.current) {
Expand All @@ -308,7 +316,6 @@ export default function TabBar<T extends Route>({
}

if (isWidthDynamic && !hasMeasuredTabWidths) {
// When tab width is dynamic, only adjust the scroll once we have all tab widths and layout
return;
}

Expand Down Expand Up @@ -376,11 +383,24 @@ export default function TabBar<T extends Route>({

// 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 @@ -495,6 +515,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 @@ -540,6 +579,8 @@ export default function TabBar<T extends Route>({
keyboardShouldPersistTaps="handled"
scrollEnabled={scrollEnabled}
bounces={bounces}
initialNumToRender={MEASURE_PER_BATCH}
onViewableItemsChanged={handleViewableItemsChanged}
alwaysBounceHorizontal={false}
scrollsToTop={false}
showsHorizontalScrollIndicator={false}
Expand Down
13 changes: 8 additions & 5 deletions packages/react-native-tab-view/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 6345fac

Please sign in to comment.