Setting up React Native (Part 5)

Setting up React Native

In the last part of this series we’ve finished our API for our stock portfolio. Today we’re going to start building our mobile app using React Native. We’ll start by setting up a new project using React Native, after which we’ll implement a basic login screen with login logic. This will cover the full spectrum, from building screens to calling our API for logging in.

Set up React Native

Install Expo

To make development with React Native a bit easier we’re going to use a framework called Expo. This framework consists of a set of tools to make developing, building and releasing React Native apps a lot easier.

Let’s install it globally by running the following command.

npm install --global expo-cli
Code language: Bash (bash)

When running this command you might get an permission error. If that’s the case run the same command using sudo.

Create a new app

Now it’s time to initialize our app. Expo made this really easy for us, we can just run the following command and answer a few questions.

expo init app
Code language: Bash (bash)

Because I’m going to develop the app using Typescript, I’ll choose blank (TypeScript). This will create the app in a directory called app and install some dependencies.

After everything is installed, you can start the development server using expo start. Alternatively, if you’re using WSL you can run expo start --tunnel. This will open a browser window with a QR code in the lower left part of your screen.

Download the Expo Go app on your phone and scan the provided QR code. This will establish a connection with your phone app, and you will be ready to start development.

UI System

I’m going to use a UI system with a lot of components built in. For this project I’ve chosen Nativebase which is a Mobile-first component library.

Let’s install native base by running the following commands in the app folder.

yarn add native-base styled-components styled-system expo install react-native-svg expo install react-native-safe-area-context
Code language: Bash (bash)

Now that Nativebase and its dependencies is installed we need to wrap our app content in a NativeBaseProvider element to make use of the provided components. Open App.tsx and add the highlighted lines.

import {StatusBar} from 'expo-status-bar'; import React from 'react'; import {StyleSheet, Text, View} from 'react-native'; import {NativeBaseProvider} from "native-base"; export default function App() { return ( <NativeBaseProvider> <View style={styles.container}> <Text>Open up App.tsx to start working on your app!</Text> <StatusBar style="auto"/> </View> </NativeBaseProvider> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, });
Code language: JavaScript (javascript)

Redux

To make managing the state of our app easier we’re going to use Redux and Redux Toolkit. So let’s install those packages.

yarn add react-redux @reduxjs/toolkit
Code language: Bash (bash)

Since Redux uses a store to manage the state of the application we’ll need to configure one. Let’s create src/redux/store.ts and add the following code.

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; // Add your reducers here const reducer = {}; export const store = configureStore({ reducer, devTools: process.env.NODE_ENV !== 'production', }); export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType<typeof store.getState>; export type AppThunk<ReturnType = void> = ThunkAction< ReturnType, RootState, unknown, Action<string> >;
Code language: TypeScript (typescript)

This configures a very basic store, without reducers. We’ll be adding those soon, don’t worry!

Now we need to tell our application about the existence of Redux and the store to use. For that we need to add a few lines to our App.tsx file.

import {StatusBar} from 'expo-status-bar'; import React from 'react'; import {StyleSheet, Text, View} from 'react-native'; import {NativeBaseProvider} from "native-base"; import {Provider} from "react-redux"; import {store} from "./src/redux/store"; export default function App() { return ( <Provider store={store}> <NativeBaseProvider> <View style={styles.container}> <Text>Open up App.tsx to start working on your app!</Text> <StatusBar style="auto"/> </View> </NativeBaseProvider> </Provider> ); } // ...
Code language: JavaScript (javascript)

This change will make sure our application knows which store to use and how to dispatch state changing events.

Create typed dispatch and selector hooks

Because we’re using TypeScript for our application we need to define the type when using useSelector and useDispatch. To make our lives a bit easier we’re going to create special pre-typed versions of those hooks.

Create a folder called hooks and add the following files.

useAppDispatch.ts

import { useDispatch } from 'react-redux'; import type { AppDispatch } from '../redux/store'; export const useAppDispatch = () => useDispatch<AppDispatch>();
Code language: TypeScript (typescript)

useAppSelector.ts

import { TypedUseSelectorHook, useSelector } from 'react-redux'; import type { RootState } from '../redux/store'; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Code language: TypeScript (typescript)

Now whenever we need to select something from the Redux store or we need to dispatch something to the store we can use these 2 hooks we’ve just created. This saves us a lot of Typescript errors when developing.

Navigation / Routing

Even in mobile apps we need a form of routing. This is used to navigate between different screens (pages). For this we will use React Navigation. Install React Navigation by executing the following commands in your app folder.

yarn add @react-navigation/native @react-navigation/native-stack expo install react-native-screens react-native-safe-area-context
Code language: Bash (bash)

After installing the dependencies we need to implement the router in our app. For this we’ll create src/router/Router.tsx with the following content.

import * as React from "react"; import LoginScreen from "../screens/public/LoginScreen"; import {createNativeStackNavigator} from "@react-navigation/native-stack"; export default function Router() { const Stack = createNativeStackNavigator(); return ( <Stack.Navigator initialRouteName="Login" screenOptions={{ headerShown: false, gestureEnabled: true, }} > <Stack.Screen name="Login" component={LoginScreen} /> </Stack.Navigator> ); }
Code language: JavaScript (javascript)

Here we are creating a Router with a single route called Login. This route refers to a component called LoginScreen which we will create later on. We are using a StackNavigator. This is a navigation component of React Navigation which will ‘stack’ the different screens on top of eachother in the app. Other navigation components like the DrawerNavigator or the BottomTabsNavigator are also available.

Now we need to tell our app to use the router we just created to render our application. Do this by editing App.tsx and replace it with the following content.

import React from 'react'; import {NativeBaseProvider} from "native-base"; import {Provider} from "react-redux"; import {store} from "./src/redux/store"; import { NavigationContainer } from '@react-navigation/native'; import Router from "./src/router/Router"; export default function App() { return ( <Provider store={store}> <NavigationContainer> <NativeBaseProvider> <Router /> </NativeBaseProvider> </NavigationContainer> </Provider> ); }
Code language: JavaScript (javascript)

This will use our router to render our application and allows for navigating between different screens.

Login screen

Let’s start building our app. The first screen a user would see is the login screen, since they’ll need an account to be able to create their portfolio. So let’s create the login screen.

I’d like to organize my screens based on accessibility like ‘public’ and ‘private’ screens. So let’s create a folder screens in the src folder. Within the screens folder create a folder called public. This folder will contain the screens that are public, for example the Login and register screens.

Now we are ready to create our login screen. Create a file called LoginScreen.tsx in the src/screens/public folder and add the following content.

import * as React from "react"; import {Box, Button, FormControl, Heading, HStack, Input, Link, VStack, Text} from "native-base"; export default function LoginScreen() { return ( <Box safeArea flex={1} p="2" py="8" w="90%" mx="auto"> <Heading size="lg" fontWeight="bold"> Sign in </Heading> <VStack space={3} mt="5"> <FormControl> <FormControl.Label> Email address </FormControl.Label> <Input type="text"/> </FormControl> <FormControl> <FormControl.Label> Password </FormControl.Label> <Input type="password" /> </FormControl> <Button mt="2" colorScheme="red" _text={{ color: 'white' }}> Sign in </Button> <HStack mt="6" justifyContent="center"> <Link _text={{ fontSize: 'sm', fontWeight: 'medium', color: 'red.500' }} href="#"> Forgot Password? </Link> <Text> | </Text> <Link _text={{ fontSize: 'sm', fontWeight: 'medium', color: 'red.500'}} href="#"> Sign Up </Link> </HStack> </VStack> </Box> ); }
Code language: JavaScript (javascript)

This creates a basic login screen with a few input fields for the email address and password, a forgot password link and a sign up link.

Setting up React Native (Part 5)

‘Private’ screen

After the user logged in we’ll need a screen which is private. For now we’re just going to add a placeholder page, which we will replace in a future article with something more useful.

Create a private folder in the src/screens folder and add a file called DashboardScreen.tsx with the following content.

import * as React from "react"; import {Box, Heading} from "native-base"; export default function DashboardScreen() { return ( <Box safeArea flex={1} p="2" py="8" w="90%" mx="auto"> <Heading size="lg" fontWeight="bold"> Signed in! </Heading> </Box> ); }
Code language: JavaScript (javascript)

For now this will just show a message telling the user they signed in successfully.

Handling user authentication

Authentication state

First we need to create a way to store the current authentication state of the user. Because we want to access this state in a lot of places in our application, a good place to store this is the Redux store. Let’s make this happen!

We’re going to make use of Redux slices which will handle a lot of boilerplate code for us. Create a file called auth.ts in the src/redux/state folder and add the following content.

import {createSlice, Draft} from '@reduxjs/toolkit'; export enum LoginStatus { LOGGED_IN, NOT_LOGGED_IN, } export interface AuthState { token: string | null; status: LoginStatus; } const initialState: AuthState = { status: LoginStatus.NOT_LOGGED_IN, token: null, }; export const authSlice = createSlice({ name: 'auth', initialState, reducers: { logout: (state: Draft<AuthState>) => { state.token = null; state.status = LoginStatus.NOT_LOGGED_IN; }, }, }); export const { logout } = authSlice.actions; export default authSlice.reducer;
Code language: TypeScript (typescript)

This file creates a few things. It defines the contents of the auth slice and the initial values. Then a slice is created using the createSlice function, which takes an object of parameters. One of the parameters are the reducers which contains an object of reducer functions to manage the state changes.

The createSlice function automatically creates actions for these reducers which are exported from the file together with the reducers.

You might expect a login reducer here, but this is not necessary because we’re going to implement this in a different way later on.

Next we need to add the exported reducer to our store. Open src/redux/store.ts and add our auth reducer to the reducers object.

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; import authReducer from './state/auth'; // Add your reducers here const reducer = { auth: authReducer, }; export const store = configureStore({ reducer, devTools: process.env.NODE_ENV !== 'production', }); // ...
Code language: TypeScript (typescript)

Implement our API

We can now store the login state. Next we need a way to actually log the user in using the /token endpoint of our API.

To create our API we’re going to use RTK Query of the Redux Toolkit library. Check out my other article about this by clicking the link.

Let’s start by creating our API base to which the different endpoints will be added to. Create a file src/redux/api/api.ts with the following content.

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import {RootState} from "../store"; // Initialize an empty api service that we'll inject endpoints into later as needed const baseApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8000/', prepareHeaders: (headers, { getState }) => { const token = (getState() as RootState).auth.token; if (token) { headers.set('Authorization', `Bearer ${token}`) } return headers; } }), endpoints: () => ({}), }); export default baseApi;
Code language: TypeScript (typescript)

This will be our API Base object which makes sure the JWT token is injected when it’s available. It also gives us one place to store the baseUrl for our api, this makes changing it later on really easy.

We can extend this baseApi object if needed. For now we need to add the authentication endpoints to get JWT tokens when the user logs in. Create auth.ts in src/redux/api and add the following content.

import api from "./api"; import TokenResponse from "../../api/responses/auth/TokenResponse"; import TokenRequest from "../../api/requests/auth/TokenRequest"; const authApi = api.injectEndpoints({ endpoints: build => ({ login: build.mutation<TokenResponse, TokenRequest>({ query: (credentials) => ({ url: '/token', method: 'POST', body: credentials, }), }), }), }); export const { useLoginMutation } = authApi; export default authApi;
Code language: TypeScript (typescript)

This defines a login method for us to use in our application via the useLoginMutation hook which is exported from the file.

You might have noticed we referenced TokenRequest and TokenResponse in the code. This is because we’re using Typescript and we want to type our request and response accordingly which makes the code a lot less error prone.

Create the files src/api/request/auth/TokenRequest.ts and src/api/response/auth/TokenResponse.ts with the following content, respectively.

type TokenRequest = { email: string, password: string, }; export default TokenRequest;
Code language: TypeScript (typescript)
type TokenResponse = { token: string, } export default TokenResponse;
Code language: TypeScript (typescript)

Now we’ve created our API and implemented the login action, we need to tell our store about its existence. We need to add some code to src/redux/store.ts to make this happen. We only need to add the reducer and the middleware from our baseApi since the extra files extend from this API. To make these changes add the highlighted lines to src/redux/store.ts.

import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; import api from './api/api'; import authReducer from './state/auth'; // Add your reducers here const reducer = { [api.reducerPath]: api.reducer, auth: authReducer, }; export const store = configureStore({ reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware), devTools: process.env.NODE_ENV !== 'production', }); // ...
Code language: TypeScript (typescript)

Update the state on login

We now need to update the authentication state of Redux when the API call to get a token succeeds. To make this happen we can make use of the actions that are dispatched by RTK Query automatically. We need to update the auth slice we created to listen for this action. The action we want to add a reducer for is authApi.endpoints.login.matchFulfilled this action is dispatched when the login API call succeeds.

We can add an extra element to the configuration of the auth slice called extraReducers. This element allows for external reducers to change the state of a slice. Add the following highlighted code to the auth slice in src/redux/state/auth.ts.

import {createSlice, Draft} from '@reduxjs/toolkit'; import authApi from "../api/auth"; // ... export const authSlice = createSlice({ name: 'auth', initialState, reducers: { logout: (state: Draft<AuthState>) => { state.token = null; state.status = LoginStatus.NOT_LOGGED_IN; }, }, extraReducers: (builder) => { builder.addMatcher( authApi.endpoints.login.matchFulfilled, (state: Draft<AuthState>, { payload }) => { state.token = payload.token; state.status = LoginStatus.LOGGED_IN; } ); } }); // ...
Code language: TypeScript (typescript)

Now whenever we call the login hook and the call succeeds, the token gets stored in the auth state and the status is set to LoginStatus.LOGGED_IN.

Implement the login functionality

Everything we need is now set up, so we can continue actually implementing the login functionality. For this we need to use the useLoginMutation hook exported by the authApi we just implemented. We also need to store the email address and the password somewhere when they’re entered by the user. In this case we’ll use the local state of the LoginScreen by using the useState hook. To perform the login, we’ll add an onPress handler to the ‘Sign in’ button.

Update src/screens/public/LoginScreen.tsx with the highlighted code below to implement this.

import * as React from "react"; import {Box, Button, FormControl, Heading, HStack, Input, Link, VStack, Text} from "native-base"; import {useLoginMutation} from "../../redux/api/auth"; export default function LoginScreen() { const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const [login, { isLoading }] = useLoginMutation(); const handleLogin = async () => { try { await login({email, password}).unwrap(); } catch (e) { // TODO: Handle errors console.log(e); } }; return ( <Box safeArea flex={1} p="2" py="8" w="90%" mx="auto"> <Heading size="lg" fontWeight="bold"> Sign in </Heading> <VStack space={3} mt="5"> <FormControl> <FormControl.Label> Email address </FormControl.Label> <Input type="text" onChangeText={(value) => setEmail(value)}/> </FormControl> <FormControl> <FormControl.Label> Password </FormControl.Label> <Input type="password" onChangeText={(value) => setPassword(value)} /> </FormControl> <Button disabled={isLoading} mt="2" colorScheme="red" onPress={handleLogin}> {isLoading ? <Text color="white">Loading...</Text> : <Text color="white">Sign in</Text>} </Button> <HStack mt="6" justifyContent="center"> <Link _text={{ fontSize: 'sm', fontWeight: 'medium', color: 'red.500' }} href="#"> Forgot Password? </Link> <Text> | </Text> <Link _text={{ fontSize: 'sm', fontWeight: 'medium', color: 'red.500'}} href="#"> Sign Up </Link> </HStack> </VStack> </Box> ); }
Code language: JavaScript (javascript)

Now that we can login we’ll need to update the router of our application to make sure only logged in users can access the private pages. We’ll fetch the current loginStatus from the Redux store using the useAppSelector hook. Depending on the value of the loginStatus we’ll load different screens in our navigator. We could even return different navigators based on this.

To implement this functionality, update src/router/Router.tsx with the following highlighted code.

import * as React from "react"; import LoginScreen from "../screens/public/LoginScreen"; import {createNativeStackNavigator} from "@react-navigation/native-stack"; import {useAppSelector} from "../hooks/useAppSelector"; import {LoginStatus} from "../redux/state/auth"; import DashboardScreen from "../screens/private/DashboardScreen"; export default function Router() { const Stack = createNativeStackNavigator(); const loginStatus = useAppSelector(state => state.auth.status); return ( <Stack.Navigator initialRouteName="Main" screenOptions={{ headerShown: false, gestureEnabled: true, }} > {loginStatus === LoginStatus.LOGGED_IN && ( <Stack.Screen name="Main" component={DashboardScreen} /> )} {loginStatus === LoginStatus.NOT_LOGGED_IN && ( <Stack.Screen name="Main" component={LoginScreen} /> )} </Stack.Navigator> ); }
Code language: JavaScript (javascript)

Now when you login you’ll see the temporary dashboard screen we created earlier.

That’s it for this part of the series. In the next part we’ll add more functionality to our app, like account registration and activation. For this we’ll implement the Linking feature of the Expo framework. Stay tuned!

Leave a Reply

Your email address will not be published.