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

What are the Alternatives of device UDID in iOS? - iOS7 / iOS 6 / iOS 5 – Get Device Unique Identifier UDID

Get Device Unique Identifier UDID Following code will help you to get the unique-device-identifier known as UDID. No matter what iOS user is using, you can get the UDID of the current iOS device by following code. - ( NSString *)UDID { NSString *uuidString = nil ; // get os version NSUInteger currentOSVersion = [[[[[UIDevice currentDevice ] systemVersion ] componentsSeparatedByString: @" . " ] objectAtIndex: 0 ] integerValue ]; if (currentOSVersion <= 5 ) { if ([[ NSUserDefaults standardUserDefaults ] valueForKey: @" udid " ]) { uuidString = [[ NSUserDefaults standardDefaults ] valueForKey: @" udid " ]; } else { CFUUIDRef uuidRef = CFUUIDCreate ( kCFAllocatorDefault ); uuidString = ( NSString *) CFBridgingRelease ( CFUUIDCreateString ( NULL ,uuidRef)); CFRelease (uuidRef); [[ NSUserDefaults standardUserDefaults ] setObject: uuidString ForKey: @" udid " ]; [[ NSUserDefaults standardUserDefaults ] synchro...

16 AWS Gotchas

16 AWS Gotchas In January I launched the MVP for my own startup,  Proximistyle , which helps you find what you’re looking for nearby. On advice from friends and industry contacts I chose AWS as my cloud provider. Having never had to set up my own cloud infrastructure before, the learning curve to get from no experience to a stable VPC system I was happy with was significantly steeper than expected, and had its fair share of surprises. #1 Take advantage of the free resources offered AWS offers a free tier for new accounts. If you have recently bought a domain and set up a company you qualify for the free tier for a year. Additionally, if you are a bootstrapped startup you can apply for  the Startup Builders package  and get $1000 in AWS credits. After doing the above, you’re now ready to get started with setting up the AWS infrastructure for your startup. #2 Set up billing budgets and alerting The very first thing you should do after setting up billing, is enabling a budge...

Ultimate Folder Structure For Your React Native Project

  Ultimate Folder Structure For Your React Native Project React native project structure React Native is a flexible framework, giving developers the freedom to choose their code structure. However, this can be a double-edged sword for beginners. Though it offers ease of coding, it can soon become challenging to manage as your project expands. Thus, a structured folder system can be beneficial in many ways like better organization, simplified module management, adhering to coding practices, and giving a professional touch to your project. This write-up discusses a version of a folder arrangement that I employ in my React Native projects. This structure is based on best practices and can be modified to suit the specific needs of your project. Before we get into the project structure let’s give credit to @sanjay who has the original idea of the structure but I modify his version of the code, to make it better. Base library axios  — For network calling. react-navigation ...