Create an Authorization Flow with React-Navigation 5.x
Tags: reactnative, reactnavigation, webdev, javascript
Published: 2020-04-11 21:45:13

## Overview

React navigation is one of the go to libraries to help build fluid react-native screens. Its easy to setup and is completely customizable. React-navigation allows you to build different types of navigators like top and bottom tabs, and side drawers. React Navigation 5.0 brings with it many additions to the library, such as dynamic screens and support for the latest react features like hooks.

In this guide, we will walk through how to build an authorization flow for your react-native app.

![Alt Text](https://embeddednature.com/wp-content/uploads/2020/04/auth-Demo-1.gif)

## Assumptions

I wont' go through basic react-native setup, I'm assuming you have a hello-world app running and want to use react-navigation.

Note: I have ejected expo but this shouldn't affect you.

## Setup

Lets start by installing react-navigation 5.0 core dependencies into your react-native application

```
npm install react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
```

next we will need to install `indicative` and `react-native-elements`

```
npm i indicative react-native-elements
```

Ensure your package.json have dependencies below:

```
"@react-navigation/stack": "5.1.0",
"@react-navigation/drawer": "^5.1.0",
"react-native-elements": "1.2.7",
"indicative": "^7.4.4",
```

## Screens

The `signUp` & `signIn` screens will use `indicative` for input validation, and `react-native-elements` to build the screens.

The screens leverage `useContext` to get state from the `app.js` `createContext`, this will pass data around our application to decide which screen the user should be on.

The screens will need to validate users email format and password lengths, this is the job of indicative. Once input validation has succeeded, we  trigger an authorizing check, before allowing a user to visit the home screen. 

here is a snippet of the indicative helper function.

```javascript
import { validateAll } from 'indicative/validator';

const handleSignUp = () => {
    const rules = {
        email: 'required|email',
        password: 'required|string|min:6|max:40|confirmed'
    };

    const data = {
        email: emailAddress,
        password: password,
        password_confirmation: passwordConfirm
    };

    const messages = {
        required: field => `${field} is required`,
        'username.alpha': 'Username contains unallowed characters',
        'email.email': 'Please enter a valid email address',
        'password.min':
            'Password is too short. Must be greater than 6 characters',
        'password.confirmed': 'Passwords do not match'
    };

    validateAll(data, rules, messages)
        .then(() => {
            console.log('success sign in');
            signUp({ emailAddress, password });
        })
        .catch(err => {
            const formatError = {};
            err.forEach(err => {
                formatError[err.field] = err.message;
            });
            setSignUpErrors(formatError);
        });
};
```

### SignUp Screen

The SignUpScreen displays a few input boxes and a button, it will call the helper function `handleSignUp()` to validate users input, once they click the sign up button. If the user intended to visit the Sign In screen, we call the `singIn` context.

We set `useEffect` to update the screen when an error occurs.

<img src="https://dev-to-uploads.s3.amazonaws.com/i/vndtor1ezxnfra5pef3s.png" width="400">

Here is the complete `signUpScreen.js`,

```javascript
import React, { useEffect, useState, useContext } from 'react';
import { validateAll } from 'indicative/validator';
import { View, Text } from 'react-native';
import {
    Input,
    Card,
    FormValidationMessage,
    Button
} from 'react-native-elements';

import { AuthContext } from '../../../utils/authContext';

const SignUpScreen = ({ navigation }) => {
    const [emailAddress, setemailAddress] = useState('');
    const [password, setPassword] = useState('');
    const [passwordConfirm, setPasswordConfirm] = useState('');
    const [SignUpErrors, setSignUpErrors] = useState({});

    const { signUp, signIn } = useContext(AuthContext); // should be signUp

    const handleSignUp = () => {
        // https://indicative.adonisjs.com
        const rules = {
            email: 'required|email',
            password: 'required|string|min:6|max:40|confirmed'
        };

        const data = {
            email: emailAddress,
            password: password,
            password_confirmation: passwordConfirm
        };

        const messages = {
            required: field => `${field} is required`,
            'username.alpha': 'Username contains unallowed characters',
            'email.email': 'Please enter a valid email address',
            'password.min':
                'Password is too short. Must be greater than 6 characters',
            'password.confirmed': 'Passwords do not match'
        };

        validateAll(data, rules, messages)
            .then(() => {
                console.log('success sign in');
                signUp({ emailAddress, password });
            })
            .catch(err => {
                const formatError = {};
                err.forEach(err => {
                    formatError[err.field] = err.message;
                });
                setSignUpErrors(formatError);
            });
    };

    useEffect(() => {}, [SignUpErrors]);

    return (
        <View style={{ paddingVertical: 20 }}>
            <Card>
                <Input
                    label={'Email'}
                    placeholder="Email address..."
                    value={emailAddress}
                    onChangeText={setemailAddress}
                    errorStyle={{ color: 'red' }}
                    errorMessage={SignUpErrors ? SignUpErrors.email : null}
                />
                <Input
                    label={'Password'}
                    placeholder="Password.."
                    value={password}
                    onChangeText={setPassword}
                    secureTextEntry
                />
                <Input
                    label={'Password Confirm'}
                    placeholder="Enter password again"
                    value={passwordConfirm}
                    onChangeText={setPasswordConfirm}
                    secureTextEntry
                />
                <Text style={{ color: 'red', marginLeft: 10, fontSize: 10 }}>
                    {SignUpErrors ? SignUpErrors.password : null}
                </Text>

                <Button
                    buttonStyle={{ margin: 10, marginTop: 50 }}
                    backgroundColor="#03A9F4"
                    title="SIGN UP"
                    onPress={() => handleSignUp()}
                />
                <Text style={{ marginLeft: 80 }} onPress={() => signIn()}>
                    Already Signed Up? Sign In
                </Text>
            </Card>
        </View>
    );
};

export default SignUpScreen;
```

## SignIn Screen

The signInScreen is very similar, with the difference being the click message users see to sign up if they don't have an account already. We use context to call `singUp`, sending an empty payload to our reducer in `app.js`

<img src="https://dev-to-uploads.s3.amazonaws.com/i/usbblx2i7m6l7k0o3y0r.png" width="400">

Here is the complete `signInScreen.js`,

```javascript
import React, { useEffect, useState, useContext } from 'react';
import { validateAll } from 'indicative/validator';
import { View, Text } from 'react-native';
import {
    Input,
    Card,
    FormValidationMessage,
    Button
} from 'react-native-elements';

import { AuthContext } from '../../../utils/authContext';

const SignInScreen = ({ navigation }) => {
    const [emailAddress, setemailAddress] = useState('');
    const [password, setPassword] = useState('');
    const [SignUpErrors, setSignUpErrors] = useState({});

    const { signIn, signUp } = useContext(AuthContext);

    const handleSignIn = () => {
        // https://indicative.adonisjs.com
        const rules = {
            email: 'required|email',
            password: 'required|string|min:6|max:40'
        };

        const data = {
            email: emailAddress,
            password: password
        };

        const messages = {
            required: field => `${field} is required`,
            'username.alpha': 'Username contains unallowed characters',
            'email.email': 'Please enter a valid email address',
            'password.min': 'Wrong Password?'
        };

        validateAll(data, rules, messages)
            .then(() => {
                console.log('success sign in');
                signIn({ emailAddress, password });
            })
            .catch(err => {
                const formatError = {};
                err.forEach(err => {
                    formatError[err.field] = err.message;
                });
                setSignUpErrors(formatError);
            });
    };

    return (
        <View>
            <Card>
                <Input
                    label={'Email'}
                    placeholder="Email"
                    value={emailAddress}
                    onChangeText={setemailAddress}
                    errorStyle={{ color: 'red' }}
                    errorMessage={SignUpErrors ? SignUpErrors.email : null}
                />
                <Input
                    placeholder="Password"
                    value={password}
                    onChangeText={setPassword}
                    secureTextEntry
                    errorStyle={{ color: 'red' }}
                    errorMessage={SignUpErrors ? SignUpErrors.password : null}
                />
                <Button
                    buttonStyle={{ margin: 10, marginTop: 50 }}
                    title="Sign in"
                    onPress={() => handleSignIn()}
                />
                <Text style={{ marginLeft: 100 }} onPress={() => signUp()}>
                    No Acount? Sign Up
                </Text>
            </Card>
        </View>
    );
};

export default SignInScreen;
```

Now with our screens complete we can build our main application to navigate between the screens.

## App.js

`App.js` will leverage some react hook features.

`createContext`: shares data between components without us needed to pass it down.

```javascript
const AuthContext = createContext({});
```

`useEffect`: fetches the token from storage or presents the signUp screen if none is found.

```javascript
useEffect(() => {
    // Fetch the token from storage then navigate to our appropriate place
    const bootstrapAsync = async () => {
        let userToken;

        try {
            userToken = await AsyncStorage.getItem('userToken');
        } catch (e) {
            // Restoring token failed
        }
        dispatch({ type: 'RESTORE_TOKEN', token: userToken });
    };
    bootstrapAsync();
}, []);
```

`useMemo`: used to memorize our context functions to avoid calling them on every render. UseMemo will only recompute when one of the inputs has changed.

```javascript
const authContextValue = useMemo(
    () => ({
        signIn: async data => {
            if (
                data &&
                data.emailAddress !== undefined &&
                data.password !== undefined
            ) {
                dispatch({ type: 'SIGN_IN', token: 'Token-For-Now' });
            } else {
                dispatch({ type: 'TO_SIGNIN_PAGE' });
            }
        },
        signOut: async data => {
            dispatch({ type: 'SIGN_OUT' });
        },

        signUp: async data => {
            if (
                data &&
                data.emailAddress !== undefined &&
                data.password !== undefined
            ) {
                dispatch({ type: 'SIGNED_UP', token: 'dummy-auth-token' });
            } else {
                dispatch({ type: 'TO_SIGNUP_PAGE' });
            }
        }
    }),
    []
);
```

`useReducer`: handles our complex state logic, setting flags that determine which screen to display.

```javascript
const [state, dispatch] = useReducer(
    (prevState, action) => {
        switch (action.type) {
            case 'TO_SIGNUP_PAGE':
                return {
                    ...prevState,
                    isLoading: false,
                    isSignedUp: false,
                    noAccount: true
                };
            case 'TO_SIGNIN_PAGE':
                return {
                    ...prevState,
                    isLoading: false,
                    isSignedIn: false,
                    noAccount: false
                };
            case 'RESTORE_TOKEN':
                return {
                    ...prevState,
                    userToken: action.token,
                    isLoading: false
                };
            case 'SIGNED_UP':
                return {
                    ...prevState,
                    isSignedIn: true,
                    isSignedUp: true,
                    isLoading: false,
                    userToken: action.token
                };
            case 'SIGN_IN':
                return {
                    ...prevState,
                    isSignedOut: false,
                    isSignedIn: true,
                    isSignedUp: true,
                    userToken: action.token
                };
            case 'SIGN_OUT':
                return {
                    ...prevState,
                    isSignedOut: true,
                    isSignedIn: false,
                    isSignedUp: true,
                    userToken: null
                };
        }
    },
    {
        isLoading: true,
        isSignedOut: false,
        isSignedUp: false,
        noAccount: false,
        isSignedIn: false,
        userToken: null
    }
);
```

### Using Navigator

Now we can call the react-navigator to build our screens

In your app.js import the navigators and initialize them

```javascript
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { createDrawerNavigator, DrawerItems } from '@react-navigation/drawer';

const Stack = createStackNavigator();
const Drawer = createDrawerNavigator();
```

### Home Stack

The homepage will build a stack that has a side drawer component.

Our home screen navigation stack will look like this:

```javascript
const createHomeStack = () => {
    const { signOut } = useContext(AuthContext);

    return (
        <Stack.Navigator>
            <Stack.Screen
                name="Home Screen"
                component={createDrawer}
                initialParams={{ singOut: signOut }}
            />
        </Stack.Navigator>
    );
};
```

Here is our side drawer navigator:

```javascript
const createDrawer = () => {
    const { signOut } = useContext(AuthContext);

    return (
        <Drawer.Navigator>
            <Drawer.Screen
                name="Home Screen"
                component={HomeScreen}
                initialParams={{
                    id: 111,
                    SignOutButton: () => (
                        <Button
                            title="Sign Me out"
                            onPress={signOut}
                            color={styles.signOutBtn.color}
                        />
                    )
                }}
            />
            <Drawer.Screen name="Screen1" component={Screen1} />
            <Drawer.Screen name="Screen2" component={Screen2} />
        </Drawer.Navigator>
    );
};
```

You will need to build the screen components (HomeScreen, Screen1, Screen2, SplashScreen) and import them into your app.js

Here is a basic example of a screen:

```javascript
import React from 'react';
import { View, Text } from 'react-native';

const aScreen = () => {
    return (
        <View style={styles.center}>
            <Text style={styles.title}>Screen2 </Text>
        </View>
    );
};

export default aScreen;
```

To render our Navigators in `App.js` we need to wrap our screens in a `NavigationContainer` but to also handle our data sharing we will need to wrap everything inside our `AuthContext` provider.

```javascript
return (
    <AuthContext.Provider value={authContextValue}>
        <NavigationContainer>
            <Stack.Navigator>{chooseScreen(state)}</Stack.Navigator>
        </NavigationContainer>
    </AuthContext.Provider>
);
```

The two helper functions were created to help us dictate which screen gets rendered in our authorization flow.

`stateConditionString()`: returns a single value, telling our switch case which Stack Screen to return. If you look at our useReducer, when the useContext is called from one of our pages it will dispatch the action and update state. The state flags is how we determine which page to naviagte to.

```javascript
export const stateConditionString = state => {
    let navigateTo = '';
    if (state.isLoading) {
        navigateTo = 'LOAD_APP';
    }
    if (state.isSignedIn && state.userToken && state.isSignedUp) {
        navigateTo = 'LOAD_HOME';
    }
    if (!state.isSignedUp && state.noAccount) {
        navigateTo = 'LOAD_SIGNUP';
    }
    if (!state.isSignedIn && !state.noAccount) {
        navigateTo = 'LOAD_SIGNIN';
    }
    return navigateTo;
};
```

`chooseScreen()`: When we define screens like this, React Navigation will only see the valid screen. This makes it impossible to navigate to the HomeScreen when the user is not signed in.

```javascript
const chooseScreen = state => {
    let navigateTo = stateConditionString(state);
    let arr = [];

    switch (navigateTo) {
        case 'LOAD_APP':
            arr.push(<Stack.Screen name="Splash" component={SplashScreen} />);
            break;

        case 'LOAD_SIGNUP':
            arr.push(
                <Stack.Screen
                    name="SignUp"
                    component={SignUpScreen}
                    options={{
                        title: 'Sign Up',
                        // When logging out, a pop animation feels intuitive
                        animationTypeForReplace: state.isSignout
                            ? 'pop'
                            : 'push'
                    }}
                />
            );
            break;
        case 'LOAD_SIGNIN':
            arr.push(<Stack.Screen name="SignIn" component={SignInScreen} />);
            break;

        case 'LOAD_HOME':
            arr.push(
                <Stack.Screen
                    name="Home"
                    component={createHomeStack}
                    options={{
                        title: 'Home Screen Parent',
                        headerStyle: { backgroundColor: 'black' },
                        headerTintColor: 'white'
                    }}
                />
            );
            break;
        default:
            arr.push(<Stack.Screen name="SignIn" component={SignInScreen} />);
            break;
    }
    return arr[0];
};
```

At this point we have everything we need to build our authentication flow. I'd like to point out that we are not using a real token. In a production app, we need to send the data to server and get a token. You'll also need to handle errors if sign in failed.

![Alt Text](https://embeddednature.com/wp-content/uploads/2020/04/auth-Demo-2.gif)

### Wrap Up 🎉

Building an authentication flow with react navigation 5.0 is straight to the point with the power of react hooks. We can define different screens based on conditions and use the react navigations stack history manager to help us guard against un authorized page access.

I also encourage you to checkout Indicative, it has some powerful features to help validate and sanitize your data.

I hope this guide has helped you understand how authorization screens can be created in react-native using the lastest features of react and react-navigation. I'd enjoy hearing about your experience in the comments below.

Feel free to reach out if you'd like some help. [Discord](https://bit.ly/EN_server)

[GitHub Repository](https://github.com/ICeZer0/NativeAuthApp)

[Twitter](https://twitter.com/EmbeddedNature)
[Instagram](https://www.instagram.com/embeddednature/)