Hola hola,
Often times, an app needs top tabs.
There are existing solutions and we've used material-top-tab-navigator for the past few years.
In an effort to use more locally defined components, we've switched to a simple <Tabs />
component.
Please note that there is no 'swipe' support at this time but would be fun to add.
Here is the Tabs
component:
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {
Dimensions,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
type Props = {
onChange: (index: number) => void;
items: string[];
};
export const Tabs = (props: Props) => {
const {onChange, items = []} = props;
const [activeTab, setActiveTab] = useState(0);
const scrollRef = useRef<ScrollView | null>(null);
const handleTabChange = useCallback(
(index: number) => {
setActiveTab(index);
onChange(index);
if (scrollRef.current) {
/**
* If you have a lot of tabs, then you need to make sure that tabs on the edges
* are shown as the User scrolls through the tabs.
*
* Without this logic, the final tabs may never be pressed on unless the User
* knows to manually scroll to the end of the tabs list.
*/
if (index > 2) {
// Scroll to the 'end' of the tabs list
scrollRef.current.scrollToEnd({animated: true});
} else {
// Scroll to the 'start' of the tabs list
scrollRef.current.scrollTo({x: 0, animated: true});
}
}
},
[onChange],
);
// Divide a given width into equal parts of items.length
const itemWidth = useMemo(() => {
const width = Dimensions.get('window').width;
return width / items.length;
}, [items.length]);
return (
<View>
<ScrollView
ref={scrollRef}
style={styles.scrollView}
contentContainerStyle={styles.container}
horizontal
showsHorizontalScrollIndicator={false}>
{items.map((item, index) => (
<TabItem
key={index}
text={item}
activeTab={activeTab}
index={index}
onPress={handleTabChange}
itemWidth={itemWidth}
/>
))}
</ScrollView>
</View>
);
};
type TabItemProps = {
text: string;
activeTab: number;
index: number;
onPress: (index: number) => void;
itemWidth: number;
};
const TabItem = (props: TabItemProps) => {
const {text, activeTab, index, onPress, itemWidth} = props;
const isActive = activeTab === index;
const minWidth = useMemo(() => Math.max(itemWidth, 80), [itemWidth]);
return (
<TouchableOpacity
style={[styles.item, isActive ? styles.selectedItem : {}, {minWidth}]}
onPress={() => onPress(index)}>
<Text style={styles.itemText}>{text}</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
scrollView: {},
container: {
alignItems: 'center',
height: 50,
},
item: {
height: 50,
minWidth: 80,
paddingHorizontal: 10,
justifyContent: 'center',
},
selectedItem: {
borderBottomWidth: 2,
},
itemText: {
alignSelf: 'center',
textAlign: 'center',
},
});
Using this component is very easy:
import React, {useState} from 'react';
import {Text, View} from 'react-native';
import {Tabs} from './Tabs';
const TABS = ['Tab 1', 'Tab 2', 'Tab 3', 'Tab 4', 'Tab 5'];
export const App = () => {
const [selectedTab, setSelectedTab] = useState(0)
return (
<View>
<Tabs items={TABS} onChange={setSelectedTab} />
{/* List of screens/tabs */}
{selectedTab === 0 && <TabOne />}
{selectedTab === 1 && <TabTwo />}
{selectedTab === 2 && <TabThree />}
{selectedTab === 3 && <TabFour />}
{selectedTab === 4 && <TabFive />}
</View>
);
};
const TabOne = () => {
return (
<View>
<Text>Tab One</Text>
</View>
);
};
const TabTwo = () => {
return (
<View>
<Text>Tab Two</Text>
</View>
);
};
const TabThree = () => {
return (
<View>
<Text>Tab Three</Text>
</View>
);
};
const TabFour = () => {
return (
<View>
<Text>Tab Four</Text>
</View>
);
};
const TabFive = () => {
return (
<View>
<Text>Tab Five</Text>
</View>
);
};
Summary
This custom <Tabs />
component can be changed to match your theme/designs/use case and will still remain very simple and easy to use.
I've been working with React Native for the last 4 years and will continue documenting common React Native errors that we come across at TroutHouseTech.
-Matt
Top comments (0)