Motivation and Introduction

As React Native Apps get bigger and move into production, code must manage a growth of the state. This state can include server response data, cached data and locally created data. User Interface state is also getting more complicated. We need to manage things like selected tabs, spinners, pagination controls etc.

Managing this wild state is hard. Changing a state to re-render a component that will update several other states will be chaotic. It will get too hard to reproduce bugs or even add new features.

A combination of React and Redux provides a perfect demarcation between the complexities of an app and state management. Redux attempts to make state updates predictable by imposing certain restrictions on how and when updates can happen.

This tutorial will focus on creating a simple React App that exculsively uses Redux to manage the state. The Redux Store will be the single source of state data truth.

Concepts Covered

  1. Redux Set Up
  2. Mapping Redux Data to Component State

Prerequisites

  1. Knowledge of JavaScript
  2. Knowledge of React Native

Step 1. Set Up the React Native Project

Start a new React Native project.

You can refer to this tutorial to install any required prerequisites tools and start a new project

Step 2. Define the Folder Structure

In the root folder of your new project, create a folder called src and define it as follows:

.
├── actions
│   ├── homePageActions.js
│   ├── index.js
│   ├── landingPageActions.js
│   └── types.js
├── App.js
├── components
│   └── pages
│   ├── HomePage.js
│   ├── LandingPage.js
│   └── MoreDetailsPage.js
├── reducers
│   ├── homePageReducer.js
│   ├── index.js
│   └── landingPageReducer.js
└── Router.js

The Router.js file will be used to Register all components that will serve as Pages thus enabling routing.

The App.js file will be used to render the Router component.

The components/pages folder will contain components that will serve as full page views.

The actions folder will serve as a definition and aggregation folder for the Redux actions.

The reducers folder will serve as a definition and aggregation folder for the Redux reducers.

Step 3. Update the index.js

Update index.js to render the new app component in the src/App.js file we created with the following code:


import {AppRegistry} from 'react-native';
import App from './src/App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);

Step 4. Set up the Routes

Set up native routing by installing the following required dependencies using the commands in the project root folder:


 npm install --save @react-navigation/native

 npm install --save react-native-screens
 
 npm install --save react-native-gesture-handler 
 
 npm install --save react-native-reanimated 


Install in the project root folder the react-native-router-flux that will handle routing using the command:


npm install --save react-native-router-flux

N/B Kindly note that React Navigation is a very mature library and can be solely used for complex navigation.

In the src/Router.js file add the following code to help set up View Scenes.
The scene with the initial prop will always render first.
The hideNavBar prop in the code below is used to hide the default header that comes with react-native-router-flux.


import React from 'react';
import { Scene, Router } from 'react-native-router-flux';

import LandingPage from './components/pages/LandingPage';
import HomePage from './components/pages/HomePage';
import MoreDetailsPage from './components/pages/MoreDetailsPage';

const RouterComponent = () => {

    return (
        <Router>
            <Scene key="root" hideNavBar>

                <Scene key="all">

                    <Scene
                        key="LandingPage"
                        component={LandingPage}
                        hideNavBar
                        initial
                    />

                    <Scene
                        key="HomePage"
                        component={HomePage}
                        hideNavBar
                    />
                    
                      <Scene
                        key="MoreDetaisPage"
                        component={MoreDetailsPage}
                        hideNavBar
                       
                    />


                </Scene>

            </Scene>
        </Router>
    );

};

export default RouterComponent;

Step 5. Install Redux

In the project root folder, install React native redux tools using the following command:

npm install --save react-redux redux redux-thunk

Step 6. Set up App.js to Create the Redux Store and Render Router.js

Since the src/Routes.js file is responsible for routing between different page components, it is the only component that will be rendered by the src/App.js.

The Router.js fine will be rendered 'inside' the created Redux store as shown below so that all the components can access the store. This is enabled by the Router component being rendered inside the Provider component.

Define App.js as follows:


import React, { Component } from 'react';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import ReduxThunk from 'redux-thunk';
import reducers from './reducers';
import Router from './Router';


class App extends Component {

    render() {

        const store = createStore(reducers, {}, applyMiddleware(ReduxThunk));
        return (

            <Provider store={store}>

                <Router />

            </Provider>

        );
    }
}


export default App; 

Step 7. Set up Minimalist Redux Actions

Actions are payloads of information that send data from your application to your store. Functions that create actions are called action creators.

The actions folder is structured as shown below:

.
├── homePageActions.js
├── index.js
├── landingPageActions.js
└── types.js

7.1 Define types.js

The file actions/types.js defines the types that the actions will use to store/update store data and will have the following code:


//action types for landing page actions
export const TYPENAME = 'typeName';
export const STORENAMESUCCESS = 'storeNameSuccess';
export const STORENAMEFAIL = 'storeNameFail';
export const STORINGNAME = 'storingName'

7.2 Define landingPageActions.js

The file actions/landingPageActions.js defines the action creators and the actions they dispatch to the store.

Import the created types and create the action creators as shown below.

The typeName action creator will be called when the user types and will update the store with the typed text.

The storeName action creator will pass the typed user name to the phone storage.
It will first dispatch the STORENAME type to show storing is in progress then will call either the storingNameSuccess or the storingNameFail action creators.

Define the landingPageActions.js as follows:


import {
    TYPENAME,
    STORENAMEFAIL,
    STORENAMESUCCESS,
    STORINGNAME
} from './types';


import { Actions } from 'react-native-router-flux';


export const typeName = (name) => {
    return ({
        type: TYPENAME,
        payload: name
    })
}


export const storeName = (name) => {

    return (dispatch) => {

        dispatch({
            type: STORINGNAME
        });

        //Async function to store the name will be created here.
        //if successfull we will call the storingNameSuccess action creator
        //if unsuccessfull we will call the storingNameFail action creator
         setTimeout(() => {

            if (name !== '') {
                storingNameSuccess(name, dispatch);
            } else {
                storingNameFail("Message is Empty", dispatch);
            }

        }, 5000);


    };

};



const storingNameSuccess = (name, dispatch) => {

    dispatch({
        type: STORENAMESUCCESS,
        payload: name
    })

    Actions.HomePage();

}

const storingNameFail = (errorMessage, dispatch) => {

    dispatch({
        type: STORENAMEFAIL,
        payload: errorMessage
    })

}

7.3 Define index.js

The index.js file is used to summarize the exportation of the action creators:

Define it as follows:


export * from './landingPageActions';
//export * from './homePageActions';

Step 8. Set up Minimalist Redux Reducers

Reducers specify how the app state will change in response to actions dispatched to the store using the defined types.
Remember that actions only describe what happened, but don't describe how the application's state changes.

The reducers folder is as structured below:

.
├── homePageReducer.js
├── index.js
└── landingPageReducer.js

8.1 Define the landingPageReducer.js

Import the created types as shown in step 7.1.

Initialize the Redux store with the keys/fields name, loading (with a default value of false) and error described below:

Fields Description && Usage
name hold user typed name
loading show storage activity, render a spinner
error show storage error message

The store keys must be unique and should not be repeated in any other reducer files.

Use an if-else or a switch case statement to determine what type was dispatched by the action creator and update the relevant data in the store as shown below:


import {
    STORENAMEFAIL,
    STORENAMESUCCESS,
    STORINGNAME,
    TYPENAME
} from '../actions/types';

const INITIAL = { name: '', loading: false, error: '' };


export default (state = INITIAL, action) => {

switch (action.type) {

case STORENAMEFAIL:
    return { ...state, loading: false, error: action.payload };

case STORENAMESUCCESS:
    return { ...state, loading: false, name: action.payload, error: '' };

case STORINGNAME:
    return { ...state, loading: true, error: '' };

case TYPENAME:
    return { ...state, loading: false, name: action.payload, error: '' };

default:
    return state;
}


};

8.2 Define index.js

Remember we only have one store for our data, so we need to combine all the reducers into 'one'

Combine the reducers as shown below by adding the following code in the Reducer/index.js:


import { combineReducers } from 'redux';
import landingPageReducer from './landingPageReducer';

export default combineReducers({
    landingPageReducer
});

Step 9. Define the Landing Page

Define the landing page as shown below or in any way you deem fit!
After all Redux is really about the concepts. 😄

More info about creating React components can be found in this article.


import React, { Component } from 'react';

import {
    View,
    StyleSheet,
    StatusBar,
    SafeAreaView,
    TextInput,
    Text,
    Dimensions,
    TouchableOpacity
} from 'react-native';



class LandingPage extends Component {

constructor() {
    super();
}

render() {

    const {
        mainContainer,
        textInputContainer,
        textTextInputSpecs,
        letsGoButtonContainer,
        letsGoButtonText
    } = styles;

return (

<>
<StatusBar backgroundColor="#ef473a" barStyle="light-content" />
<SafeAreaView style={{ flex: 1 }}>
    <View style={mainContainer}>

        <View style={textInputContainer}>
            <TextInput
                placeholder="name"
                placeholderTextColor="rgba(204, 50, 50, 0.56)"
                // value={this.props.inputText}
                // onChangeText={value => function(value)}
                style={textTextInputSpecs}

            />

        </View>

         <TouchableOpacity onPress={() => this.props.storeName(this.props.name)}>
            <View style={letsGoButtonContainer}>

                <Text style={letsGoButtonText}>
                    Cool Lets Go
                </Text>

            </View>
        </TouchableOpacity>
    </View>
</SafeAreaView>
</>

);
}
}


export default LandingPage;

const {
    height, width
} = Dimensions.get('screen');


const styles = StyleSheet.create({

    mainContainer: {
        flex: 1,
        backgroundColor: '#b2b2b2',
        alignItems: 'center',
        justifyContent: 'center'
    },

    textInputContainer: {
        width: width * 0.75,
        backgroundColor: 'white',
        borderRadius: width * 0.1,
        marginVertical: height * 0.0125,
        alignItems: 'center',
        justifyContent: 'center',
    },

    textTextInputSpecs: {
        color: '#cb2d3e',
        fontSize: height * 0.03,
        paddingVertical: height * 0.0125,


    },

    letsGoButtonContainer: {
        width: width * 0.45,
        borderRadius: width * 0.1,
        borderRadius: width * 0.1,
        marginVertical: height * 0.0125,
        paddingVertical: height * 0.0125,
        backgroundColor: '#ef473a',
        alignItems: 'center',
        justifyContent: 'center',

    },
    spinnerContainer: {
        width: width * 0.45,
        borderRadius: width * 0.1,
        borderRadius: width * 0.1,
        marginVertical: height * 0.0125,
        paddingVertical: height * 0.0125,
        alignItems: 'center',
        justifyContent: 'center',

    },
    letsGoButtonText: {
        fontSize: height * 0.03,
        color: 'white',

    },

})

Step 10. Run the App

At this point, the app should start.
You can refer to this article to start the App or correct some common errors you may encounter.

Step 11. Utilize the Action Creators

In step 7 above, we defined our action creators.
We need to import them and utilize them in our components.

In the Landing page defined in step 9 above, we have two main activities that should update data in the store:

  1. typing text
  2. pressing a button

11.1 Typing Text

Import the action creator functions in the Landing page as shown below:

import {
    typeName,
    storeName
} from '../../actions/landingPageActions';

Call the typeName function whenever text is input. This will in turn dispatch an update to the store and update the user text.

Update the text input section as shown below:

...
 <View style={textInputContainer}>
        <TextInput
           ...
            onChangeText={value => this.props.typeName(value)}
            style={textTextInputSpecs}

        />

 </View>
 ...

Add the name value from the store. Since we are working with store, the value of the name text input needs to come from the store.

Update the text input value as follows:

<View style={textInputContainer}>
        <TextInput
           ...
            value={this.props.name}
            onChangeText={value => this.props.typeName(value)}
            style={textTextInputSpecs}

        />

 </View>

By now you must have already noticed values from store are in the props object.
How do they become props?
This is achieved using the connect function.

Import connect in the Landing page:


...
import { connect } from 'react-redux';
...

Create the actual connection:

Remove the line:

export default LandingPage;

and replace it with:


....

const mapStateToProps = ({ landingPageReducer }) => {

    const { name, error, loading } = landingPageReducer;

    return { name, error, loading };

};

export default connect(mapStateToProps, { typeName, storeName })(LandingPage);

...

The connect will always fetch data from the store via the landing page reducer and mapping that data to the state.

This implies that any change in the store data will trigger a mounted component to re-render.

11.2 Let's Go Button

Update the Button Section in the landing page as follows:


...

<TouchableOpacity onPress = {() => this.props.storeName()}>
    <View style={letsGoButtonContainer}>

        <Text style={letsGoButtonText}>
            Cool Lets Go
        </Text>

    </View>
</TouchableOpacity>
...

12. Run the App

Refer to step 10 above and the app's landing page will be as shown below:
Landing Page

Step 13. Displaying the Error Message from the Store

Display the error message by creating a function to render the optional message as follows:


...

renderErrorMessage() {
    const {
        error
    } = this.props

    const {
        loginErrorText
    } = styles;

    if (error !== '') {
        return (
            <Text style={loginErrorText}>
                {error}
            </Text>
        );
    }
}
    
...

Render the function above the button section in the Landing page as shown below:

...
{this.renderErrorMessage()}

<TouchableOpacity onPress={() => this.props.storeName(this.props.name)}>
    <View style={letsGoButtonContainer}>

        <Text style={letsGoButtonText}>
            Cool Lets Go
        </Text>

    </View>
</TouchableOpacity>
....

Step 14. Displaying a Spinner, when the Button is Pressed

It makes sense to use a spinner to prevent button multi presses.

Use the loading prop from the Redux store to decide whether or not the spinner should render.

Create a decider function as shown below:


    ...

renderButtonOrSpinner() {

const {
    loading
} = this.props;

const {
    spinnerContainer,
    letsGoButtonContainer,
    letsGoButtonText
} = styles;

if (loading) {
    return (
        <View style={spinnerContainer}>
            <ActivityIndicator
                size="large"
                color="#cb2d3e"
            />

        </View>
    );
} else {
    return (
        <TouchableOpacity onPress={() => this.props.storeName(this.props.name)}>
            <View style={letsGoButtonContainer}>

                <Text style={letsGoButtonText}>
                    Cool Lets Go
                        </Text>

            </View>
        </TouchableOpacity>
    );
}

}
    
    ...

And replace the button section of the Landing Page with:


 ...

 {this.renderButtonOrSpinner()}
 
 ...
 

Step 15. Access Store Data in the HomePage

The whole point of a store is so that different components can access the store properties and map to their states.

Define the Homepage as shown below:


import React, { Component } from 'react';

import {
    View,
    Dimensions,
    Text,
    StyleSheet,
    StatusBar,
    SafeAreaView,

} from 'react-native';

import { connect } from 'react-redux';



class HomePage extends Component {

    constructor() {
        super();
    }

    render() {

        const {
            styleText,
            mainContainer
        } = styles;


        return (

            <>
                <StatusBar backgroundColor="#ef473a" barStyle="light-content" />
                <SafeAreaView style={{ flex: 1 }}>
                    <View style={mainContainer}>

                        <Text style={styleText}>
                            {this.props.name}
                        </Text>

                        <Text style={styleText}>
                            {this.props.loading.toString()}
                        </Text>

                        <Text style={styleText}>
                            {this.props.error}
                        </Text>



                    </View>
                </SafeAreaView>
            </>

        );
    }
}

const mapStateToProps = ({ landingPageReducer }) => {

    const { name, error, loading } = landingPageReducer;

    return { name, error, loading };

};

export default connect(mapStateToProps, {})(HomePage);



const {
    height, width
} = Dimensions.get('screen');


const styles = StyleSheet.create({

    mainContainer: {
        flex: 1,
        backgroundColor: '#b2b2b2',
        alignItems: 'center',
        justifyContent: 'space-around'
    },

    styleText: {
        color: 'red',
        fontSize: height * 0.05,
    },


})

The page should print data directly extracted from the store as shown in the demo:
Homepage

Conclusion, Next steps and Remarks

React Native is a notable open-source mobile application framework relevant in development of cross-platform applications. However as the applications become bigger and complex there is need for state management. This is where the popular Redux library comes in hand to provide elements for state management in React Native. Redux has powerful debugging, a well structured and methodical state management and is directly testable.

For the next steps, play around with Redux store data, store objects and arrays as opposed to strings and integers. Observe component re-rendering after changes in the Redux data store.

This tutorial goes through a step by step creation of an app and more focus is into state management in React Native using Redux library.

For functional React, React Context is a very good state store.

Find more code on Github

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.