We're planting a tree for every job application! Click here to learn more

Persisting User authentication in a React-Native App

David Abimbola

18 Jun 2021

5 min read

Persisting User authentication in a React-Native App
  • Redux

blog1.png

It is very necessary to provide ways to authenticate your users in your application. Finding working examples online has been tough so I thought writing about it will be a good idea.

The Idea

The idea was gotten here react-navigation but this has some downside. After a User successfully login a token is returned from the API-server and this token is saved in AsyncStorage.

AsyncStorage is a simple, unencrypted, asynchronous, persistent, key-value storage system that is global to the app. It should be used instead of LocalStorage.

For more info on AsyncStorage visit react-native.

The token is a credential used to access protected content from the API-server. Giving a scenario where the token needs to be added to the headers before making an API request. Yes, the token is saved in AsyncStorage but we would need to get the token from AsyncStorage first then attach it to the headers before making the request.

Giving a Scenario where we need to make 50 AJAX request in our application and each time we do this we get the Token from AsyncStorage.Getting the token from a AsyncStorage is a synchronous operation and it takes time before the token is gotten. This can be be quite annoying and can reduce our app performance. Why can’t we just make the synchronous request once instead of making it 50 times and save it somewhere, then we get it from there anytime we need it.

My Solution

Saving the token in Redux or using the new React Context-API sounds like a good idea.Wait!!! what is Redux ?

Redux can be said to be a predictable state container for JavaScript apps. If you are new to Redux I will suggest you visit redux for more Information. Context provides a way to pass data through the component tree without having to pass props down manually at every level. You can find more information about React Context-API here react.

Kent C. Dodds also has a good article about it, visit here for more info.

Wes Bos also has a good tutorial, visit here for more info.

Code

We will be using the code in react-navigation website as a boiler plate. It can be found here.

Lets first create our Redux solution. we will definitely need to install redux, react-redux, and redux-thunk into our boiler plate code as shared above.

npm i redux react-redux redux-thunk

I assume we already know how redux providers and redux store works so i will just go ahead with actions.js and reducers.js

Here is our actions.js:

import { AsyncStorage } from 'react-native';

export const getToken = (token) => ({
    type: 'GET_TOKEN',
    token,
});

export const saveToken = token => ({
    type: 'SAVE_TOKEN',
    token
});

export const removeToken = () => ({
    type: 'REMOVE_TOKEN',
});

export const loading = bool => ({
    type: 'LOADING',
    isLoading: bool,
});

export const error = error => ({
    type: 'ERROR',
    error,
});



export const getUserToken = () => dispatch => 

 AsyncStorage.getItem('userToken')
        .then((data) => {
            dispatch(loading(false));
            dispatch(getToken(data));
        })
        .catch((err) => {
            dispatch(loading(false));
            dispatch(error(err.message || 'ERROR'));
        })



export const saveUserToken = (data) => dispatch =>
    AsyncStorage.setItem('userToken', 'abc')
        .then((data) => {
            dispatch(loading(false));
            dispatch(saveToken('token saved'));
        })
        .catch((err) => {
            dispatch(loading(false));
            dispatch(error(err.message || 'ERROR'));
        })

export const removeUserToken = () => dispatch =>
    AsyncStorage.removeItem('userToken')
        .then((data) => {
            dispatch(loading(false));
            dispatch(removeToken(data));
        })
        .catch((err) => {
            dispatch(loading(false));
            dispatch(error(err.message || 'ERROR'));
        })

reducers.js:

import { combineReducers } from 'redux';

const rootReducer = (state = {
    token: {},
    loading: true,
    error: null,
}, action) => {
    switch (action.type) {
        case 'GET_TOKEN':
            return { ...state, token: action.token };
        case 'SAVE_TOKEN':
            return { ...state, token: action.token };
        case 'REMOVE_TOKEN':
            return { ...state, token: action.token };
        case 'LOADING':
            return { ...state, loading: action.isLoading };
        case 'ERROR':
            return { ...state, error: action.error };
        default:
            return state;
    }
};

export default combineReducers({
    token: rootReducer
});

AuthLoading.js:

import React from 'react';
import {
    ActivityIndicator,
    AsyncStorage,
    StatusBar,
    StyleSheet,
    View,
} from 'react-native';
import { connect } from 'react-redux';
import { getUserToken } from '../actions';

class AuthLoadingScreen extends React.Component {
    static navigationOptions = {
        header: null,
    };
    constructor() {
        super();
    }

    componentDidMount() {
        this._bootstrapAsync();
    }

    // Fetch the token from storage then navigate to our appropriate place
    _bootstrapAsync = () => {

        this.props.getUserToken().then(() => {
            this.props.navigation.navigate(this.props.token.token !== null ? 'App' : 'Auth');
        })
            .catch(error => {
                this.setState({ error })
            })

    };

    // Render any loading content that you like here
    render() {
        return (
            <View style={styles.container}>
                <ActivityIndicator />
                <StatusBar barStyle="default" />
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
    },
});

const mapStateToProps = state => ({
    token: state.token,
});


const mapDispatchToProps = dispatch => ({
    getUserToken: () => dispatch(getUserToken()),
});

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

We can see from the above that our state is handled by redux and our token is also stored in redux. The below shows how our token can be gotten from redux store.

import React from 'react';
import {
    AsyncStorage,
    Button,
    StatusBar,
    StyleSheet,
    View,
    Text,
} from 'react-native';

import { connect } from 'react-redux';
import { removeUserToken } from '../actions';

class OtherScreen extends React.Component {
    static navigationOptions = {
        title: 'Lots of features here',
    };

    render() {
        return (
            <View style={styles.container}>
                <Button title="I'm done, sign me out" onPress={this._signOutAsync} />
                <StatusBar barStyle="default" />
                <Text>  Here is the user token {this.props.token.token}</Text>
            </View>
        );
    }

    _signOutAsync =  () => {
        this.props.removeUserToken()
            .then(() => {
                this.props.navigation.navigate('Auth');
            })
            .catch(error => {
                this.setState({ error })
            })
    };
}


const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '# fff',
        alignItems: 'center',
        justifyContent: 'center',
    },
});

const mapStateToProps = state => ({
    token: state.token,
});

const mapDispatchToProps = dispatch => ({
    removeUserToken: () => dispatch(removeUserToken()),
});

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

The full source code for the redux integration can be found here.

Lets see how we can achieve the same thing with React Context-API.

We define provider.js like this:

import { AsyncStorage } from 'react-native';

export const MyContext = React.createContext();


export default class MyProvider extends Component {
    state = {
        token: '',
        saveToken: async () => {
            try {
                const resp = await AsyncStorage.setItem('userToken', 'abc');
                return resp;
            }
            catch (error) {
                this.setState({ error })
            }

        },
        removeToken: async () => {
            try {
                const resp = await AsyncStorage.removeItem('userToken');
                return resp
            }
            catch (error) {
                this.setState({ error })
            }
        },
        getToken: async () => {
            try {
                const resp = await AsyncStorage.getItem('userToken');
                return resp;
            }
            catch (error) {
                this.setState({ error })
            }
        }

    }


    componentWillMount() {
        AsyncStorage.getItem('userToken')
            .then((token) => {
                this.setState({ token })
            })
            .catch(error => {
                this.setState({ error })
            })
    }

    render() {
        return (
            <MyContext.Provider value={this.state}>
                {this.props.children}
            </MyContext.Provider>
        );
    }
}

We would need to wrap our parent container with the Provider component like this:

import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { AppNavigator} from './navigator';
import MyProvider from './Provider';


export default () => (
  <MyProvider>
    <View style={styles.container}>
      <AppNavigator />
    </View>
  </MyProvider>
)

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

Our AuthLoadingScreen would look like this; it's quite complex so I advise you spend some time on this:

import React from 'react';
import {
    ActivityIndicator,
    AsyncStorage,
    StatusBar,
    StyleSheet,
    View,
} from 'react-native';
import { MyContext } from '../Provider';


const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
    },
});

class AuthLoadingScreen extends React.Component {
    static navigationOptions = {
        header: null,
    };
    constructor() {
        super();

    }
    componentDidMount() {
        this._bootstrapAsync();
    }

    // Fetch the token from storage then navigate to our appropriate place
    _bootstrapAsync = async () => {

        this.props.context.getToken()
            .then((userToken) => {
                // This will switch to the App screen or Auth screen and this loading
                // screen will be unmounted and thrown away.
                this.props.navigation.navigate(userToken ? 'App' : 'Auth');
            })
            .catch(error => {
                this.setState({ error });
            })

    };

    render() {
        return (
            <View style={styles.container}>
                <MyContext.Consumer>
                    {context => ((
                        <View>
                            <ActivityIndicator />
                            <StatusBar barStyle="default" />
                        </View>
                    ))}
                </MyContext.Consumer>
            </View>
        );
    }
};


const ForwardRef = React.forwardRef((props, ref) => (
    <MyContext.Consumer>
        {context => <AuthLoadingScreen context={context} {...props} />}
    </MyContext.Consumer>
));

export default ({ navigation }) => (
    <View style={styles.container}>
        <ForwardRef
            navigation={navigation}
        />
    </View>

)

Now we have our token saved in Context, let's see how we can get it!

import React from 'react';
import {
    AsyncStorage,
    Button,
    StatusBar,
    StyleSheet,
    View,
    Text,
} from 'react-native';
import { MyContext } from '../Provider';

export default class OtherScreen extends React.Component {
    static navigationOptions = {
        title: 'Lots of features here',
    };

    render() {
        return (
            <View style={styles.container}>
                <MyContext.Consumer>
                    {context => ((
                        <View>
                            <Button title="I'm done, sign me out" onPress={() => this._signOutAsyn(removeToken)} />
                            <Text> Here is your token  {context.token} </Text>
                            <StatusBar barStyle="default" />
                        </View>
                    ))}
                </MyContext.Consumer>
            </View>
        );
    }

    _signOutAsync = async (removeToken) => {
        removeToken()
            .then(() => {
                this.props.navigation.navigate('Auth');
            })
            .catch(error => {
                this.setState({ error })
            })
    };
}


const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '# fff',
        alignItems: 'center',
        justifyContent: 'center',
    },
});

The full Context-API integration source code can be found here.

Wrapping Up

We have learned how to:

  1. Save our token in Redux and how to retrieve it
  2. Use React Context-API

You can find complete source code on Github here. Comments and feedback are well appreciated.

Did you like this article?

David Abimbola

Full stack javascript developer

See other articles by David

Related jobs

Title

The company

title

Remote

Title

The company

title

Remote

Title

The company

title

Remote

Title

The company

title

Remote

Related articles

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

12 Sep 2021

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

12 Sep 2021

CareersCompaniesSitemapFunctional WorksBlockchain WorksJavaScript WorksAI WorksGolang WorksJava WorksPython WorksRemote Works
email iconhello@works-hub.comUK flag

Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ

US flag

108 E 16th Street, New York, NY 10003

Subscribe to our newsletter

Join over 111,000 others and get access to exclusive content, job opportunities and more!

© 2021 WorksHub

Privacy PolicyDeveloped by WorksHub