Reusable Top Tabs in React Native

matthewzruiz

Matt Ruiz

Posted on July 3, 2023

Reusable Top Tabs in React Native

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',
  },
});

Enter fullscreen mode Exit fullscreen mode

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>
  );
};

Enter fullscreen mode Exit fullscreen mode

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

馃挅 馃挭 馃檯 馃毄
matthewzruiz
Matt Ruiz

Posted on July 3, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related