Better Modals in React Navigation

An annoying design problem with a reusable solution.

Brad Dunn
5 min readOct 3, 2017
I miss my phone jack, ok? Don’t judge me!

Update: It’s been brought to my attention that this (admittedly hacky) approach stopped working on the newest versions of React Navigation. This was always a stop-gap, so I expected this approach to eventually fail. That being said, Jaiwant Mulik came up with a solution that works on React Navigation v1.1.2, so if you are on that or a similar version this might still work for you.

In the future, I plan on writing a follow-up article with an approach that works for newer versions of React Navigation. For now, I’ve updated the code and examples to work on v1.1.2. If you aren’t running that or a similar version, your mileage may vary

React Navigation is an incredibly useful library for handling navigation in your React Native project. It can be a bit confusing at first, particularly when it comes to sharing data between screens. With some practice, however, this library becomes invaluable when handling navigation and screen transitions in your app. There is one issue, however, that has bothered me since I started using this library — and I may have a decent solution to the problem.

The issue

Often when mapping out the navigational needs of an application, it becomes necessary to have screens presented by a slide-up modal — particularly if you wish to conform to the design patterns and expectations of iOS users. In React Navigation, that means we need to reach for the StackNavigator component. A basic setup would look like this:

const ModalStack = StackNavigator({
Home: { screen: MyHomeScreen },
Profile: { screen: MyProfileScreen },
}, {
mode: 'modal',
});
Our basic modal navigation

The issue occurs when you don’t want all of your screens to be presented in a modal. React Navigation approaches this use-case by allowing us to create multiple navigators with different properties and nest them within each other. For example, I can create a basic card stack navigator for all of the main screens in my application and nest that within a separate modal-style stack navigator for all the screens I wish to present as a modal.

// Main Card-Style Navigator
const MainStack = StackNavigator({
Home: { screen: HomeScreen },
Detail: { screen: DetailScreen }
});
// Modal-Style Navigator
const ModalStack = StackNavigator({
Home: { screen: MainStack },
Modal: { screen: ModalScreen },
}, {
mode: 'modal',
});

Simple enough, right? But… we have a problem.

…?

Double Headers! All the way! What does it mean?

When we nested our two StackNavigators in the previous portion, we inadvertently asked the navigation to generate and display two separate persistent headers. Annoying, but a quick scan of the docs tells us that we can pass an additional option to our modal navigator to hide the second header:

// Modal-Style Navigator
const ModalStack = StackNavigator({
Home: { screen: MainStack },
Modal: { screen: ModalScreen },
}, {
mode: 'modal',
headerMode: 'none',
});

Great! We’re down to just one header again! But… now we don’t have a header on our modal screen. Damn!

The Solution

In order to get a header working on the modal screen we have a few options. We could roll our own header component — which is a viable option. The problem with this route is that React Navigation has spent a lot of time designing a robust header component that would be time consuming to replicate properly.

Instead, let’s grab that header component directly from the bowels of the React Navigation source code and reuse that component standalone in our modal screen!

We can find the file we need in react-navigation/src/views/Header/Header.

Disclaimer: The suggestion to pull out this view and reuse it was suggested elsewhere on the internet first. I’m having issues tracking down the original article/stack overflow that I found this suggestion in, but I will update the article with credit once I or someone else finds it.

import Header from 'react-navigation/src/views/Header/Header';class ModalScreen extends React.Component {
render() {
return (
...
<Header scene={{index: 0}}
scenes={[{index: 0, isActive: true}]}
navigation={{state: {index: 0}}}
getScreenDetails={() => ({options: {
title: 'Modal',
headerRight: (
<Button
title="Close"
onPress={() => this.props.navigation.goBack()}
/>
)
}})}
/>
...
);
}
}

Ok… it’s not pretty. But it get’s the job done, and it’s consistent and repeatable. We tell React Navigation that we want to cherry-pick their Header component, and then we confuse the heck out of the component by force-feeding it basic data — telling the component that we are on the first scene (scene index = 0) and that it can just ignore the navigation state. We additionally tell the header that we have a bunch of bogus scenes with the line scenes={[{index: 0, isActive: true}]}(thanks Jaiwant Mulik for pointing this out).We then pass in an object very similar to what we would use in the static navigationOptions on a normal screen and voila — we have a consistent header!

Takeaway

It’s hacky — its not perfect, but it gets the job done. Wrap this up in a component that’s reusable for all of your modals and call it a day like I did. Hopefully the header component is exposed with a nicer API in the future, but until this happens, this little hack isn’t that bad of a soltion!

Further Reading

Full Example in a Snack: https://snack.expo.io/H1NijfdtM

--

--