Skip to main content

How I built React Native Tab View - Top & Bottom Tab View

 How I built React Native Tab View

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 


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. 

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 


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 Comments. 😃

Thanks a lot to 

Sanjay Vaghasiya
 for editing and improving the article.

Comments

Popular Posts

How I Reduced the Size of My React Native App by 85%

How and Why You Should Do It I borrowed 25$ from my friend to start a Play Store Developer account to put up my first app. I had already created the app, created the assets and published it in the store. Nobody wants to download a todo list app that costs 25mb of bandwidth and another 25 MB of storage space. So today I am going to share with you how I reduced the size of Tet from 25 MB to around 3.5 MB. Size Matters Like any beginner, I wrote my app using Expo, the awesome React Native platform that makes creating native apps a breeze. There is no native setup, you write javascript and Expo builds the binaries for you. I love everything about Expo except the size of the binaries. Each binary weighs around 25 MB regardless of your app. So the first thing I did was to migrate my existing Expo app to React Native. Migrating to React Native react-native init  a new project with the same name Copy the  source  files over from Expo project Install all de...

How to recover data of your Android KeyStore?

These methods can save you by recovering Key Alias and Key Password and KeyStore Password. This dialog becomes trouble to you? You should always keep the keystore file safe as you will not be able to update your previously uploaded APKs on PlayStore. It always need same keystore file for every version releases. But it’s even worse when you have KeyStore file and you forget any credentials shown in above box. But Good thing is you can recover them with certain tricks [Yes, there are always ways]. So let’s get straight to those ways. 1. Check your log files → For  windows  users, Go to windows file explorer C://Users/your PC name/.AndroidStudio1.4 ( your android studio version )\system\log\idea.log.1 ( or any old log number ) Open your log file in Notepad++ or Any text editor, and search for: android.injected.signing and if you are lucky enough then you will start seeing these. Pandroid.injected.signing.store.file = This is  file path where t...

React Native - Text Input

In this chapter, we will show you how to work with  TextInput  elements in React Native. The Home component will import and render inputs. App.js import React from 'react' ; import Inputs from './inputs.js' const App = () => { return ( < Inputs /> ) } export default App Inputs We will define the initial state. After defining the initial state, we will create the  handleEmail  and the  handlePassword  functions. These functions are used for updating state. The  login()  function will just alert the current value of the state. We will also add some other properties to text inputs to disable auto capitalisation, remove the bottom border on Android devices and set a placeholder. inputs.js import React , { Component } from 'react' import { View , Text , TouchableOpacity , TextInput , StyleSheet } from 'react-native' class Inputs extends Component { state = { ...