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.
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.
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.
Here is the complete signUpScreen.js
,
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
Here is the complete signInScreen.js
,
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.
const AuthContext = createContext({});
useEffect
: fetches the token from storage or presents the signUp screen if none is found.
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.
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.
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
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:
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:
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:
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.
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.
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.
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.
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
Top comments (9)
Hi Dear, It is a great article to handle authentication with context provider. But I am using redux to manage app state. can you please guide, how i can handle authentication flow with redux.
Thanks in advance.
I must have missed something. Not understanding the initialParams signOut bit. I get a warning about "Non-serializable values were found in the navigation state". Still have found your article very informative. Thanks!
could you please share the code
Let me create a public repository and I'll share the link.
I've added a repository -> github.com/ICeZer0/NativeAuthApp
I can't believe React is so messy.. form validations and form handling is really really bad.
We need more Observables built in, working with promises is horrendous in 2020..
Hi Lars, thanks for taking the time to check out my post! You are right, keeping components in sync with state has been an issue. I think hooks has been a good addition to help, It would be interesting to see how other devs handle these scenarios. I find myself developing more defensively to protect against re-renders and side effects, its all a learning experience 😁. Any recommendations for implementing a good obverse pattern? I see many using RxJS, nothing built in though.
Thank you for the great article. Do you think it’s possible to link this with aws cognito as user Authentication?
Glad you liked it! Yes, Congito would be a good authorization gateway to use. With the AWS library, you could set the user in the Congito user pool then request a token which gets cached and validated using hooks.