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

Sentry mobile image

App store rankings love fast apps - mobile vitals can help you get there

Slow startup times, UI hangs, and frozen frames frustrate users—but they’re also fixable. Mobile Vitals help you measure and understand these performance issues so you can optimize your app’s speed and responsiveness. Learn how to use them to reduce friction and improve user experience.

Read full post →

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay