Skip to content

Commit

Permalink
fix: prevent material tabBarItem from re-rendering when using materia…
Browse files Browse the repository at this point in the history
…l-top-tabs
  • Loading branch information
okwasniewski committed May 26, 2023
1 parent 1613461 commit d5b3e41
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 72 deletions.
3 changes: 2 additions & 1 deletion packages/material-top-tabs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
},
"dependencies": {
"color": "^4.2.3",
"react-native-tab-view": "^3.5.1"
"react-native-tab-view": "^3.5.1",
"use-latest-callback": "^0.1.6"
},
"devDependencies": {
"@react-navigation/native": "workspace:^",
Expand Down
155 changes: 87 additions & 68 deletions packages/material-top-tabs/src/views/MaterialTopTabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
import Color from 'color';
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { TabBar, TabBarIndicator } from 'react-native-tab-view';
import { TabBar, TabBarIndicator, TabBarProps } from 'react-native-tab-view';
import useLatestCallback from 'use-latest-callback';

import type { MaterialTopTabBarProps } from '../types';

Expand All @@ -26,6 +27,86 @@ export function MaterialTopTabBar({
focusedOptions.tabBarInactiveTintColor ??
Color(activeColor).alpha(0.5).rgb().string();

type Props = TabBarProps<Route<string>>;

const getAccessibilityLabel = useLatestCallback<
NonNullable<Props['getAccessibilityLabel']>
>(({ route }) => descriptors[route.key].options.tabBarAccessibilityLabel);

const getTestID = useLatestCallback<NonNullable<Props['getTestID']>>(
({ route }) => descriptors[route.key].options.tabBarButtonTestID
);

const renderIcon = useLatestCallback<NonNullable<Props['renderIcon']>>(
({ route, focused, color }) => {
const { options } = descriptors[route.key];

if (options.tabBarShowIcon === false) {
return null;
}

if (options.tabBarIcon !== undefined) {
const icon = options.tabBarIcon({ focused, color });

return (
<View style={[styles.icon, options.tabBarIconStyle]}>{icon}</View>
);
}

return null;
}
);

const renderLabel = useLatestCallback<NonNullable<Props['renderLabel']>>(
({ route, focused, color }) => {
const { options } = descriptors[route.key];

if (options.tabBarShowLabel === false) {
return null;
}

const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;

if (typeof label === 'string') {
return (
<Text
style={[
{ color },
fonts.regular,
styles.label,
options.tabBarLabelStyle,
]}
allowFontScaling={options.tabBarAllowFontScaling}
>
{label}
</Text>
);
}

const children =
typeof options.tabBarLabel === 'string'
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;

return label({ focused, color, children });
}
);

const renderBadge = useLatestCallback<NonNullable<Props['renderBadge']>>(
({ route }) => {
const { tabBarBadge } = descriptors[route.key].options;

return tabBarBadge?.() ?? null;
}
);

return (
<TabBar
{...rest}
Expand All @@ -46,12 +127,8 @@ export function MaterialTopTabBar({
indicatorContainerStyle={focusedOptions.tabBarIndicatorContainerStyle}
contentContainerStyle={focusedOptions.tabBarContentContainerStyle}
style={[{ backgroundColor: colors.card }, focusedOptions.tabBarStyle]}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
}
getTestID={({ route }) =>
descriptors[route.key].options.tabBarButtonTestID
}
getAccessibilityLabel={getAccessibilityLabel}
getTestID={getTestID}
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
Expand All @@ -69,67 +146,9 @@ export function MaterialTopTabBar({
target: route.key,
})
}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];

if (options.tabBarShowIcon === false) {
return null;
}

if (options.tabBarIcon !== undefined) {
const icon = options.tabBarIcon({ focused, color });

return (
<View style={[styles.icon, options.tabBarIconStyle]}>{icon}</View>
);
}

return null;
}}
renderLabel={({ route, focused, color }) => {
const { options } = descriptors[route.key];

if (options.tabBarShowLabel === false) {
return null;
}

const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: (route as Route<string>).name;

if (typeof label === 'string') {
return (
<Text
style={[
{ color },
fonts.regular,
styles.label,
options.tabBarLabelStyle,
]}
allowFontScaling={options.tabBarAllowFontScaling}
>
{label}
</Text>
);
}

const children =
typeof options.tabBarLabel === 'string'
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;

return label({ focused, color, children });
}}
renderBadge={({ route }) => {
const { tabBarBadge } = descriptors[route.key].options;

return tabBarBadge?.() ?? null;
}}
renderIcon={renderIcon}
renderLabel={renderLabel}
renderBadge={renderBadge}
renderIndicator={({ navigationState: state, ...rest }) => {
return focusedOptions.tabBarIndicator ? (
focusedOptions.tabBarIndicator({
Expand Down
15 changes: 12 additions & 3 deletions packages/react-native-tab-view/src/TabBarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,19 @@ const MemoizedTabBarItemInternal = React.memo(
) as typeof TabBarItemInternal;

export function TabBarItem<T extends Route>(props: Props<T>) {
const { onPress, onLongPress, onLayout, navigationState, route, ...rest } =
props;
const {
onPress,
onLongPress,
onLayout,
navigationState,
route,
getAccessibilityLabel,
...rest
} = props;
const onPressLatest = useLatestCallback(onPress);
const onLongPressLatest = useLatestCallback(onLongPress);
const onLayoutLatest = useLatestCallback(onLayout ? onLayout : () => {});
const onLayoutLatest = useLatestCallback(onLayout);

Check failure on line 280 in packages/react-native-tab-view/src/TabBarItem.tsx

View workflow job for this annotation

GitHub Actions / lint

Argument of type '((event: LayoutChangeEvent) => void) | undefined' is not assignable to parameter of type 'Function'.
const getAccessibilityLabelLatest = useLatestCallback(getAccessibilityLabel);

const tabIndex = navigationState.routes.indexOf(route);

Expand All @@ -281,6 +289,7 @@ export function TabBarItem<T extends Route>(props: Props<T>) {
onLayout={onLayoutLatest}

Check failure on line 289 in packages/react-native-tab-view/src/TabBarItem.tsx

View workflow job for this annotation

GitHub Actions / lint

Type 'Function' is not assignable to type '(event: LayoutChangeEvent) => void'.
onLongPress={onLongPressLatest}
isFocused={navigationState.index === tabIndex}
getAccessibilityLabel={getAccessibilityLabelLatest}
route={route}
index={tabIndex}
routesLength={navigationState.routes.length}
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4546,6 +4546,7 @@ __metadata:
react-native-pager-view: 6.1.2
react-native-tab-view: ^3.5.1
typescript: ^4.9.4
use-latest-callback: ^0.1.6
peerDependencies:
"@react-navigation/native": ^6.0.0
react: "*"
Expand Down Expand Up @@ -20113,6 +20114,15 @@ __metadata:
languageName: node
linkType: hard

"use-latest-callback@npm:^0.1.6":
version: 0.1.6
resolution: "use-latest-callback@npm:0.1.6"
peerDependencies:
react: ">=16.8"
checksum: d94bea5cc5910d5967b12c766355c11812c1f57b5993d4b429dd6e580ac5904087ce732891d4fb3a855e0e56e6a7900b9af2084ede1d86df8e381afe47c2947a
languageName: node
linkType: hard

"use-sync-external-store@npm:^1.0.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
Expand Down

0 comments on commit d5b3e41

Please sign in to comment.