Category: Training

How to build a dynamic and scalable react native navigation flow

It took me a while to understand the complexity of the react native navigation through different navigators, nested navigators and screen, so I hope that by reading this post, you can understand a lot better this subject from my experience.

What we’re gonna do here is the following:

Packages and libraries and whatever else we’re gonna use and you should be already familiarized with:

Architecture and folder structure

├── assets
├── components
│   ├── ctrl-app-bar
│   └── ctrl-navigation
│       └── ctrl-tab-bar
├── config
│   ├── data
│   └── routes
├── containers
├── core
├── redux
│   └── app
├── screens
│   ├── favorites-screen
│   ├── home-screen
│   ├── login-screen
│   ├── master-screen
│   ├── notifications-screen
│   ├── profile-screen
│   └── settings-screen
└── util

Core files, functions and methods to build our navigation

We create the app.json file to keep the routes array object structure which should look like this:

[
    ...,
    {
        "component": "FavoritesScreenContainer",
        "icon": "heart",
        "name": "favorites",
        "navigation": ["root", "tabs"],
        "order": 1
    },
    {
        "component": "LoginScreenContainer",
        "defaultRoot": true,
        "icon": "key",
        "name": "login",
        "navigation": ["root"]
    },
    ...
]

Object keys explanation:

component – Container screen name.
icon – Tab bar icon to display.
name – Name to display to the user.
navigation – The different navigators where to include that container screen.

I include other keys such as:

defaultRoot To know which route is going to be shown as default in root navigator.
defaultTabs To know which route is going to be shown as default in tabs navigator.
order To sort the tabs however I want.

You can include the keys you want and handle whatever you need to do with them in the buildNavigation() function which is where we go now.

So, we have this function on our source/config/index.js file

const buildNavigation = (navigationFor) =>
    appRoutes.filter(route => route.navigation.includes(navigationFor))
        .map((route, index) => ({
            ...route,
            description: route.name,
            index,
            key: route.name,
            name: capitalizeFirstLetter(route.name)
        }));

And we use it like this a few lines below

const rootNavigation = buildNavigation('root');
const tabsNavigation = buildNavigation('tabs');
export default {
    initialState,
    routes: {
        rootNavigation,
        tabsNavigation
    }
}

“So, you are right, we create two separate arrays (one for root navigation and one for tabs navigation) and add new properties to each element of each array.”

Once the steps above are done we’re gonna create the navigation component in source/components/ like this:

ctrl-navigation
├── ctrl-tab-bar
│   ├── index.js
├── root-navigation.js
└── tabs-navigation.js

You can check the ctrl-tab-bar component in the github repo to deepen more into it. But in a simple way is just a custom component to render the bottom tab bar in the app. 😊

Now focus first on the tabs-navigation.js

// @scripts
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
// @constants
const Tab = createBottomTabNavigator();
const Tabs = ({
    TabBarComponent,
    initialRouteName,
    routes
}) => (
    <Tab.Navigator
        initialRouteName={initialRouteName}
        tabBar={props => {
            // over here we should return the custom
            // ctrl-tab-bar component mentioned above
            return <TabBarComponent {...props} />;
        }}
    >
        {routes.map(route => (
            <Tab.Screen
                component={route.component}
                key={route.key}
                name={route.name}
                options={{
                    icon: route.icon,
                    title: route.description
                }}
            />
        ))}
    </Tab.Navigator>
);
export default Tabs;

As you can see we receive 3 props in the Tabs component, a TabBarComponent, the initialRouteName and an array of routes which we iterate to stack all the tabs routes.

Pretty easy and understable, right?

Then go the root-navigation.js

// @packages
import { createStackNavigator } from '@react-navigation/stack';
// @scripts
import Tabs from './tabs-navigation';
// @constants
const Stack = createStackNavigator();
const Root = ({
    headerProps,
    onSetAppTitle,
    rootProps,
    tabsProps
}) => (
    <Stack.Navigator
        headerMode="float"
        initialRouteName={rootProps.initialRouteName}
    >
        <Stack.Screen
            component={() => (
                <Tabs
                    TabBarComponent={tabsProps.TabBarComponent}
                    initialRouteName={tabsProps.initialRouteName}
                    onSetAppTitle={onSetAppTitle}
                    routes={tabsProps.routes}
                />
            )}
            name="Tabs"
            options={headerProps}
        />
        {rootProps.routes.map(route => (
            <Stack.Screen
                component={route.component}
                key={route.key}
                name={route.key}
                options={headerProps}
            />
        ))}
    </Stack.Navigator>
);
export default Root;

As you can see we receive 4 props in the Root component, a headerProps, the onSetAppTitle action creator and two more objects rootPropstabsProps because this Root component is the one that we instantiate in our main container.

We put a Stack.Screen component outside the root routes iteration with the Tabs component or in other words our Tabs Navigator to start nesting this navigation flow.

Back to the source/App.js which should look like this:

// @scripts
import AppContainer from './containers/app';

export default () => <AppContainer />;

Then go to source/containers/app.js which should look like this:

// @packages
import React from 'react';
import { NavigationNativeContainer } from '@react-navigation/native';
import { Provider as PaperProvider } from 'react-native-paper';
import { Provider as ReduxProvider } from 'react-redux';

// @scripts
import MasterScreenContainer from './master-screen';
import { store } from '../core';

const AppContainer = () => (
    <ReduxProvider store={store}>
        <PaperProvider>
            <NavigationNativeContainer>
                <MasterScreenContainer />
            </NavigationNativeContainer>
        </PaperProvider>
    </ReduxProvider>
);

export default AppContainer;

Let’s keep going through the hierarchy, this is the source/containers/master-screen.js, over here we’re using some redux action creators and state reducers.

// @packages
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

// @scripts
import MasterScreen from '../screens/master-screen';
import { setAppTitle } from '../redux/app';

const MasterScreenContainer = ({
    appTitle,
    onSetAppTitle,
    userIsLoggedIn
}) => (
    <MasterScreen
        appTitle={appTitle}
        onSetAppTitle={onSetAppTitle}
        userIsLoggedIn={userIsLoggedIn}
    />
);

const mapDispatchToProps = dispatch => bindActionCreators({
    onSetAppTitle: setAppTitle
}, dispatch);

const mapStateToProps = ({ appInfo }) => ({
    appTitle: appInfo.title
});

export default connect(
    mapStateToProps, mapDispatchToProps
)(MasterScreenContainer);

Then go to the source/screens/master-screen/index.js which should look like this:

// @scripts
import CtrlAppBar from '../../components/ctrl-app-bar';
import CtrlRootNavigation from '../../components/ctrl-navigation/root-navigation';
import CtrlTabBar from '../../components/ctrl-navigation/ctrl-tab-bar';
import { config } from '../../config';
import { mapComponent } from '../../config/components/mapper';

const MasterScreen = ({
    appTitle,
    onSetAppTitle
}) => {
    // A simple function to map the component
    const prepareRoutes = routes => routes.map(route => ({
        ...route,
        component: mapComponent(route.component)
    }));

    // The root routes
    const rootRoutes = prepareRoutes(config.routes.root);

    // The initial route name for our root routes
    // Wich is probably the Login
    const rootInitialRouteName = rootRoutes.find(route => route.defaultRoot).key

    // The tabs routes
    const tabsRoutes = prepareRoutes(config.routes.tabs)
        .sort((routeA, routeB) => routeA.order - routeB.order);

    // The initial route name for our tabs routes
    // Wich is probably the Home
    const tabsInitialRouteName = tabsRoutes.find(route => route.defaultTabs).key;

    // Our tab bar component to customize the styles in it
    const TabBarComponent = props => (
        <CtrlTabBar
            iconSize={COMMON_ICON_SIZE}
            onSetAppTitle={onSetAppTitle}
            {...props}
        />
    );

    return (
        <CtrlRootNavigation
            headerProps={{
                header: () => (
                    <CtrlAppBar
                        title={appTitle}
                    />
                ),
                headerShown: appTitle !== 'login'
            }}
            onSetAppTitle={onSetAppTitle}
            rootProps={{
                initialRouteName: rootInitialRouteName,
                routes: rootRoutes
            }}
            tabsProps={{
                TabBarComponent,
                initialRouteName: tabsInitialRouteName,
                routes: tabsRoutes
            }}
        />
    );
};

export default MasterScreen;

Now we have to understand how the mapComponent() function works and it’s pretty simple, its behavior is:

// @scripts
// Import each container with the same name convention from our routes json file
import FavoritesScreenContainer from '../../containers/favorites-screen';
import LoginScreenContainer from '../../containers/login-screen';

// @constants
// Create an object of components
const components = {
    FavoritesScreenContainer,
    LoginScreenContainer
};

// Create a function with a componentName as param
// and returns the component as a Component
/**
 * @param {string} componentName
 * @returns {function}
 */
export const mapComponent = componentName =>
    components[componentName];

Now, we’re ready to go!

Notes

I did not explain some of the components and any other logic that is already in the repo to don’t make this guide long and just focus on the main subject here: Build a scalable navigation in a easy way to understand.

If you have doubts, issues or any trouble trying to make this from scratch you can go to the repo and dig deeper into the project and see how everything was done. 😊

This is the repo on github for this project 😊

Learn More