DEV Community

Matt Ruiz
Matt Ruiz

Posted on • Edited on

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

Top comments (0)