Skip to content

Commit

Permalink
fix: optimize tabBarItem by memoizing getter functions (#11427)
Browse files Browse the repository at this point in the history
**Motivation**

The goal of this PR is to memoize the result of getter functions passed
to TabBarItem like: `getAccessibilityLabel()`. This is currently _the
best_ we can do to optimize material-top-tabs re-renders.

**Test plan**

Go to Material top tabs example and examine re-renders using React
Profiler.
  • Loading branch information
okwasniewski committed Jun 21, 2023
1 parent 7d8b515 commit 1f94c8b
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 42 deletions.
92 changes: 50 additions & 42 deletions packages/react-native-tab-view/src/TabBarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import useLatestCallback from 'use-latest-callback';

import { PlatformPressable } from './PlatformPressable';
import { TabBarItemLabel } from './TabBarItemLabel';
import type { NavigationState, Route, Scene } from './types';

export type Props<T extends Route> = {
Expand Down Expand Up @@ -85,18 +86,26 @@ const getInactiveOpacity = (

type TabBarItemInternalProps<T extends Route> = Omit<
Props<T>,
'navigationState'
| 'navigationState'
| 'getAccessibilityLabel'
| 'getLabelText'
| 'getTestID'
| 'getAccessible'
> & {
isFocused: boolean;
index: number;
routesLength: number;
accessibilityLabel?: string;
label?: string;
testID?: string;
accessible?: boolean;
};

const TabBarItemInternal = <T extends Route>({
getAccessibilityLabel,
getAccessible,
getLabelText,
getTestID,
accessibilityLabel,
accessible,
label: labelText,
testID,
onLongPress,
onPress,
isFocused,
Expand Down Expand Up @@ -166,29 +175,16 @@ const TabBarItemInternal = <T extends Route>({
}
}

const renderLabel =
renderLabelCustom !== undefined
? renderLabelCustom
: (labelProps: { route: T; color: string }) => {
const labelText = getLabelText({ route: labelProps.route });

if (typeof labelText === 'string') {
return (
<Animated.Text
style={[
styles.label,
icon ? { marginTop: 0 } : null,
labelStyle,
{ color: labelProps.color },
]}
>
{labelText}
</Animated.Text>
);
}

return labelText;
};
const renderLabel = renderLabelCustom
? renderLabelCustom
: (labelProps: { color: string }) => (
<TabBarItemLabel
{...labelProps}
icon={icon}
label={labelText}
labelStyle={labelStyle}
/>
);

if (renderLabel) {
const activeLabel = renderLabel({
Expand Down Expand Up @@ -225,20 +221,16 @@ const TabBarItemInternal = <T extends Route>({

const scene = { route };

let accessibilityLabel = getAccessibilityLabel(scene);

accessibilityLabel =
typeof accessibilityLabel !== 'undefined'
? accessibilityLabel
: getLabelText(scene);
typeof accessibilityLabel !== 'undefined' ? accessibilityLabel : labelText;

const badge = renderBadge ? renderBadge(scene) : null;

return (
<PlatformPressable
android_ripple={android_ripple}
testID={getTestID(scene)}
accessible={getAccessible(scene)}
testID={testID}
accessible={accessible}
accessibilityLabel={accessibilityLabel}
accessibilityRole="tab"
accessibilityState={{ selected: isFocused }}
Expand Down Expand Up @@ -266,14 +258,31 @@ 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,
getLabelText,
getTestID,
getAccessible,
...rest
} = props;
const onPressLatest = useLatestCallback(onPress);
const onLongPressLatest = useLatestCallback(onLongPress);
const onLayoutLatest = useLatestCallback(onLayout ? onLayout : () => {});

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

const scene = { route };

const accessibilityLabel = getAccessibilityLabel(scene);
const label = getLabelText(scene);
const testID = getTestID(scene);
const accessible = getAccessible(scene);

return (
<MemoizedTabBarItemInternal
{...rest}
Expand All @@ -284,16 +293,15 @@ export function TabBarItem<T extends Route>(props: Props<T>) {
route={route}
index={tabIndex}
routesLength={navigationState.routes.length}
accessibilityLabel={accessibilityLabel}
label={label}
testID={testID}
accessible={accessible}
/>
);
}

const styles = StyleSheet.create({
label: {
margin: 4,
backgroundColor: 'transparent',
textTransform: 'uppercase',
},
icon: {
margin: 2,
},
Expand Down
39 changes: 39 additions & 0 deletions packages/react-native-tab-view/src/TabBarItemLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import type { StyleProp, ViewStyle } from 'react-native';
import { Animated, StyleSheet } from 'react-native';

interface TabBarItemLabelProps {
color: string;
label?: string;
labelStyle: StyleProp<ViewStyle>;
icon: React.ReactNode;
}

export const TabBarItemLabel = React.memo(
({ color, label, labelStyle, icon }: TabBarItemLabelProps) => {
if (!label) {
return null;
}

return (
<Animated.Text
style={[
styles.label,
icon ? { marginTop: 0 } : null,
labelStyle,
{ color: color },
]}
>
{label}
</Animated.Text>
);
}
);

const styles = StyleSheet.create({
label: {
margin: 4,
backgroundColor: 'transparent',
textTransform: 'uppercase',
},
});

0 comments on commit 1f94c8b

Please sign in to comment.