How I built React Native Tab View

Satyajit Sahoo
6 min readJul 4, 2016

--

Recently I needed a tabbed navigation for a React Native app I’m working on. Specifically, animated swipeable tabs like shown in the material design guidelines. I’ll cover my requirements and the implementation in the article, as well as a simple example on how to use it.

I needed the tab navigator to,

  • Expose a stateless (for persistence, analytics etc.) and declarative API
  • Allow swipeable tab content, a common design pattern on Android.
  • Have smooth page transitions without frame drops
  • Allow for fully customizable animation and styles
  • Apply default styles that match the platform guidelines

I found react-native-scrollable-tab-view which was started by Brent Vatne and now maintained by Evgeniy Sokovikov. built for similar user cases. However, I found it can drop frames, lacks customizability and uses a stateful API.

I also found react-swipeable-views, which has a cross platform implementation (web, ios and android), but only after I set out to work on my own implementation of a tab view.

Now, how hard could it be to implement this?

This seemed like a good exercise,given my lack of experience with React Native’s Animated (for animations in JavaScript) and PanResponder (for handling touch and gestures).

Compared to the ScrollView/ViewPager, the Animated API seemed a great choice for more flexibility. For example, building a ‘cover flow’ style tab system would be impossible. Nested ScrollViews in Android are also problematic, so using a ScrollView means inheriting those issues.

Soon after I had working, but imperfect code. Joshua Sierles needed a similar UI in his app, and helped me a lot (quite a lot) testing, improving and fixing bugs in my implementation. Open Source is awesome, isn’t it?

The implementation

When broken down, the implementation is simple enough. Let’s take a tour!

Measure the width of the container

By default, we position the pages in a horizontal view extending past the width of the screen. The current page occupies the screen width. So we need to measure the width of the container using React Native’s built-in onLayout prop.

Store the current position in an Animated.Value

This position value will be updated and tracked often, by the tapping the navigation, and by swiping content, so it makes sense that we store it in an Animated.Value.

Storing the actual translate value as the Animated.Value made sense at first, but this meant other components tracking this value had to know about the container width.

So the final implementation stores the index value, which can be a float. For example, a value of 1.5corresponds to halfway between the second and third tab.

Handling the swipe gesture

This is the trickiest part! So many things can go wrong. More detail below.

Prevent unnecessary component re-renders

When animating, we must strive to do as little work as possible to maintain 60 FPS. Re-renders during the animation are costly and can produce jitter, since the animations run on the same javascript thread.

We avoid this by updating component state only after the animation finishes, as opposed to when the gesture finishes, which other solutions seem to do.

Put everything together in a wrapper component

The TabViewTransitioner wraps everything. It’s responsible for:

  1. Measuring the layout, to properly position the pages and tab bar indicator.
  2. Providing an Animated.Value to control the gestures and animations. A single Animated.Value for everything means nothing will go out of sync. This value is available anywhere in the layout for controlling other animations.
  3. Animating the position and updating tab components when navigation state changes, ensuring animation won’t stop midway due to a race condition.
  4. Providing helper methods to nested components for changing and current position. The swipe, tab indicator and tab label opacity animation all track the Animated.Value, so they stay in sync.

Finally, we add in the TabViewPage.StyleInterpolator to control the position of a tab based on state, using interpolation on the Animated.Value. Animated is super-powerful and you can do so much using interpolation. Totally love it. ❤️

Checkout the React Native docs on Animations which covers this in detail.

Handling the Gestures

PanResponder is powerful and simple, but it’s really easy to make mistakes.

Using the PanResponder handlers we need to:

Detect ‘mostly’ horizontal movement to trigger the swipe

Since people often move their finger at an angle when scrolling, it’s easy to cause unintentional swipes. After some trial and error, I decided to go with the following check to determine horizontal movement.

In onMoveShouldSetResponder, which grants the gesture to our code, we check if the horizontal distance travelled and horizontal velocity are at least 3 times of the vertical distance and velocity.

function isMovingHorzontally(evt, gestureState) {
return (
(Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 3)) &&
(Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 3))
);
}

Allow flicking to switch tabs

To do this, we track the gesture velocity.

The gestureState vx and vy properties (horizontal and vertical velocity) are checked against a threshold.

if (Math.abs(gestureState.dx) > POSITION_THRESHOLD || Math.abs(gestureState.vx) > VELOCITY_THRESHOLD) {
// do the thing
}

Don’t swipe when a vertical gesture changes to horizontal

This prevents problems when another component might be responding to the gesture, such as a vertical ScrollView, or with a sloppy or unrelated gesture.

We handle this by tracking the initial swipe direction in onResponderMove, then check it in other handlers throughout the gesture.

Finally, we clean this up in onPanResponderRelease.

Checkout the TabViewPage.PanResponder code! It’s short and should be easy to grok.

Gotchas along the way

Gesture velocity is represented in milliseconds on iOS, but nanoseconds on Android (https://github.com/facebook/react-native/pull/8199). I didn’t realize this in the beginning, which led to issues on iOS as I was developing on Android.

Not knowing this in the beginning led to issues on iOS, since I was developing on Android.

Achieving smooth animation while manually tracking velocity can quickly get out of hand. A spring animation improves things tons, so it’s the default in TabViewTransitioner.

Wrapping it up

After all this, I got a tabbed navigator that didn’t suck! I have published the component to npm so others can use it.

Just run the following in your project to install,

npm install --save react-native-tab-view

Then you can import and use it. It’s written in pure JS: no need to link native code. Just import and you’re done!

A very simple example without the tab bars:

import React, { Component } from 'react';
import { View, StyleSheet } from 'react-native';
import { TabViewAnimated, TabViewPage, TabBarTop } from 'react-native-tab-view';

export default class TabViewExample extends Component {
state = {
index: 0,
routes: [
{ key: '1', title: 'First' },
{ key: '2', title: 'Second' },
],
};

_renderScene = ({ route }) => {
switch (route.key) {
case '1':
return <View style={{ flex: 1, backgroundColor: '#ff4081' }} />;
case '2':
return <View style={{ flex: 1, backgroundColor: '#673ab7' }} />;
default:
return null;
}
};

_renderPage = (props) => <TabViewPage {...props} renderScene={this._renderScene} />;

render() {
return (
<TabViewAnimated
style={{ flex: 1 }}
navigationState={this.state.navigation}
renderScene={this._renderPage}
renderHeader={this._renderHeader}
onRequestChangeTab={index => this.setState({ index })}
/>
);
}
}

The README contains a simple example with Material Design themed tab bars. For more advanced usage (e.g. — cover flow), check the example app.

The TabBar is super customizable and supports icons, text, and custom indicator (the tiny line below the active tab). It can be used as a top bar or a bottom bar.

The animations and gestures can be disabled, or tweaked using a custom panHandler if needed.

Note that the project is still young, and the API not finished. While it’s mostly stable, a few things might change in the future.

I update the release notes with breaking changes, so make sure to read them if you’re using the component.

I still need to polish the styles to match the platform guidelines more closely. Do send a PR if you can :D

I plan to integrate it into Exponent’s ExNavigation library so it’s easier to use. Help is very welcome!

If you decide to use it in your project, let me know how it goes. If you face a bug or have a question, please open an issue on GitHub. 😃

Thanks a lot to Joshua Sierles for editing and improving the article.

--

--

Satyajit Sahoo

Front-end developer. React Native Core Contributor. Codes JavaScript at night. Crazy for Tacos. Comic book fanatic. DC fan. Introvert. Works at @Callstackio