Re-writing React Navigation Stack

A quick introduction to our new version of the React Navigation stack navigator.

Satyajit Sahoo
Exposition

--

React Navigation consists of many navigators, such as the stack, tabs and drawer navigators. Recently I’ve been working with with Michał Osadnik on a new version of the stack navigator, which lives under the react-navigation-stack repo as a standalone package. The main goal of this new version is to improve the performance of animations during gestures, but we also took the time to improve some other aspects of the library.

In this article, we introduce you briefly to the improvements. Here’s a quick overview:

  • Performant animations and gestures
  • Improved handling of state changes
  • New transitions for the latest iOS and Android versions
  • A simpler API for building custom transitions
  • More consistent and dynamic options
  • Simpler built-in components
  • A more maintainable codebase

Performant animations and gestures

UIKit style animation for the Header

Previously, we used React Native’s Animated API for our animations. While it worked well for the most part, when dealing with gestures, there was always a visible delay between releasing the gesture and start of transition due to extra roundtrip over the bridge.

For this new version, we chose to use react-native-reanimated along with react-native-gesture-handler for native driven smooth gestures and animations.

This was a large undertaking which required a rewrite, but the results are smoother animations and a more maintainable codebase.

In addition, it’s now also possible to enable gestures for both vertical and horizontal transition animations (a highly requested feature). Gestures can also now be enabled or disabled for individual screens and each transition animation separately.

Improved handling of state changes

AKA animated replace and reset

Each navigator in React Navigation contains two parts: a router and a view. The router is responsible for managing the state (i.e. keeping the list of routes, the active index, etc.). The view receives the latest state from the router, and decides how to handle the state change.

In the current stack navigator, animations are based on index change: when the current active index changes, an animation is performed. This works great when pushing a new screen or popping an existing one.

However, things get tricky with more complex actions: for example, when you reset the state or replace a route, no animation is performed. This can feel unnatural as the visible screen changes suddenly without any intermediate states. In addition, if you do something such as adding five screens in one go, it triggers five push animations, which doesn’t look great and is often undesirable.

In the new version, we try to animate state changes more gracefully. In essence, we try to determine if we should show a push animation or a pop animation, based on what changed in the state. A couple of examples:

  1. If you replace or push a route, a push animation is performed, because a new screen was added which resembles a push.
  2. If you add/remove a bunch of routes with reset, but the focused route is the same, no animation is performed, because the visible screen didn’t change.
  3. If you remove multiple routes from the end with reset or pop, a single pop animation is performed.

Note that while I listed some actions in these examples, it doesn’t really matter which action caused the change. The animation will work the same.

The only case we can’t animate is when you re-order routes to bring an existing screen into focus without removing the previous screen.

You can control whether a screen should be animated by specifying an animationEnabled option in navigationOptions.

New transitions for the latest iOS/Android versions

iOS Modal Presentation style

In the current version of React Navigation, the stack navigator used the transition animations from iOS 12 and Android 8.

Android 9 (Pie) introduced a new default screen transition. As opposed to the previous fade in animation, this added a reveal effect. Our new stack navigator now uses this animation by default.

Meanwhile, iOS 13 introduced a new modal presentation animation where screens are stacked on top of each other like a stack of cards. The new stack navigator exports a preset for this which you can use in your apps.

Along with the new animations, we also tweaked the old animations to match the native animations more closely.

There’s also more fine grained control over the transitions and gestures. You can import various transition animations from the library and mix and match them the way you like.

A simpler API for building custom transitions

We entirely rewrote the transition API, as the old one wasn’t a good fit for react-native-reanimated. The new API turned out to be much simpler and easier to use.

To customize the transition, there are multiple config options:

  • gestureDirection: This controls the direction of the gesture to match the transition animation, possible values are horizontal and vertical.
  • transitionSpec: This is an object which specifies the animation type (timing or spring) and their options (such as duration for timing). It takes 2 properties, open for the transition when adding a screen and close for the transition when removing a screen.
  • cardStyleInterpolator: This is a function which specifies interpolated styles to animate various parts of the card, e.g. the overlay, shadow etc.
  • headerStyleInterpolator: This is a function which specifies interpolated styles to animate various parts of the header, e.g. the title, left button etc.

The library exports various sets of configs as presets for easier use. For example, using the new iOS modal presentation style is as easy as importing the preset and spreading it in your navigationOptions:

import {
createStackNavigator,
TransitionPresets,
} from 'react-navigation-stack';
const MyStack = createStackNavigator(
{
// Screens
},
{
defaultNavigationOptions: {
...TransitionPresets.ModalPresentationIOS,
cardOverlayEnabled: true,
gestureEnabled: true,
},
}
);

We’ll soon publish proper documentation for building custom transition animations in the official docs, so stay tuned.

More consistent and dynamic options

Previously, it wasn’t possible to pass many options in navigationOptions, so they needed to be static and couldn’t change based on params. They also had to be specified once per navigator and couldn’t be overridden per screen.

In the new version, most options are moved to navigationOptions. So they can be more dynamic and customizable per screen.

We also changed some options to be more consistent. For example, headerLeft took a function while headerRight took a React element. Now all of the options which render something take a function returning a React element.

Simpler built-in components

In the new version, the built-in components such as Header, HeaderBackButton, etc. are much simpler. Header now takes fewer props and is per-screen as opposed per navigator. This should make it easier to build your own header component as well as wrap the existing header.

A more maintainable codebase

In the current implementation, there were a lot of hacks to work around the limitations of Animated API. It also used unsafe lifecycle hooks which meant the code wasn’t async mode compatible.

In the new implementation, we tried to simplify everything to make it more maintainable and avoid hacks altogether. We also got rid of some redundant code (such as 2 different back button implementations.

Everything is strictly typed with TypeScript from the beginning, so it’s now much easier to refactor, while also providing great DX to the users.

Come check it out!

If you want to try the new alpha, be sure to check the release notes for upgrade instructions and a complete list of breaking changes. Testing and bug reports regarding the new versions are highly appreciated.

This work wouldn’t have been possible without Brent Vatne and sponsorship from Expo. Thanks to them for their constant contributions to improve the React Native ecosystem.

You can tweet me @satya164 if you want to chat about React Navigation, or want to get started with contributing to Open Source.

--

--

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