OOP React Native Auth Flow
I have found myself working on a React Native App with some amateur programmers.
I’ve taken it upon myself, which the team agreed, to lay the ground work when it
comes to API calls. The first mission was creating a functional login page.
Context
We’re using the React Navigation
package, their
example auth flow
looks pretty
crazy and crams a lot into that first App.js
file. I would like to hide
this complexity so none of the amateur programmers get spooked or overly
confused looking at the app’s entry point.
My main inspiration came from the accepted answer from this old StackOverflow
Post: React New Context API - Access Existing Context across Multiple Files
I also like how it is more of an OOP approach, but this might looked down upon
in the React “normal way of doing it” crowd; so be warned!
Global Context Object
I want this object to handle:
- Getting/Setting/Removing authentication token on device’s storage
- Store if the user is logged in (has token)
- One stop shop to get user’s token inside the App
This is the skeleton I have come up with:
Path: GlobalContext.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
/// <reference types="node" />
import React from "react";
import * as SecureStore from "expo-secure-store";
const GlobalContext = React.createContext({});
interface GlobalProps {
children?: React.ReactNode;
}
export interface GlobalStates {
userLoggedIn?: boolean;
isLoading?: boolean;
userToken?: string;
setToken?: (token: string) => void;
signOut?: () => void;
}
/**
* A React Component with the Content Provider embedded into it.
*
* @remarks
*
* Values can also be accessed using `React.useContext(GlobalContext)` after
* importing this file. WARNING: It must be accessed **inside** of a React
* Component.
*/
class GlobalProvider extends React.Component<GlobalProps, GlobalStates> {
/**
* Class initializer, defaults, and trigger fetching token from secure
* storage.
*
* @param props - Built in Props from React
*/
constructor(props: GlobalProps) {
super(props);
this.state = {
userLoggedIn: false,
isLoading: true,
userToken: null,
setToken: async (token: string) => {
_setToken(token);
},
signOut: async () => {
_removeToken(token);
},
};
this._getToken(); // onload, get token
}
render() {
return (
<GlobalContext.Provider
value={{
userToken: this.state.userToken,
setToken: this.state.setToken,
signOut: this.state.signOut,
isLoading: this.state.isLoading,
userLoggedIn: this.state.userLoggedIn,
}}
>
{this.props.children}
</GlobalContext.Provider>
);
}
/**
* Triggered on initial app launch.
*/
async _getToken(): Promise<void> {
try {
const token = await SecureStore.getItemAsync("myapp_access_token");
if (token !== null) {
this.setState({ userToken: token });
this.setState({ userLoggedIn: true });
}
} catch (error) {
console.error(error); // Storage Error
}
// signal fetching ended
this.setState({ isLoading: false });
}
/**
* Signin Flow. Set token into storage for future app launches.
*
* @param token - user auth token
*/
async _setToken(token: string): Promise<void> {
try {
await SecureStore.setItemAsync("myapp__access_token", token);
this.setState({ userToken: token });
this.setState({ userLoggedIn: true });
} catch (error) {
console.error(error); // Storage Error
}
}
/**
* Signout Flow. Remove token from storage, kick user back to auth
*/
async _removeToken(): Promise<void> {
try {
await SecureStore.deleteItemAsync("myapp__access_token");
this.setState({ userToken: null });
this.setState({ userLoggedIn: false });
} catch (error) {
console.error(error); // Storage Error
}
}
}
// break the consumer out of context so it can be used like the provider.
const GlobalConsumer = GlobalContext.Consumer;
// React Components to be placed at Root of App.
export { GlobalProvider, GlobalConsumer };
export default GlobalContext;
|
Note
There is one thing that isn’t quite intuitive: this.state
inside
on GlobalProvider
should be thought of as private variables. Adding the
states to the value
in the render()
function is what makes them public.
I think the nuance can be useful somehow, but I haven’t discovered a use yet.
Auth Flow
This is an example App.js
. Other components are imported from elsewhere to
keep this file even simpler and easy to understand.
Path: App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import React from "react";
import * as SplashScreen from "expo-splash-screen";
import { GlobalProvider, GlobalConsumer } from "GlobalContext";
import Main from "Main";
import Auth from "Auth";
const App = () => {
return (
<GlobalProvider>
<GlobalConsumer>
{(ctx) => {
if (ctx.isLoading) {
// `expo-splash-screen` will keep the SplashScreen showing until
// SplashScreen.hideAsync() is called
return null;
} else {
SplashScreen.hideAsync();
if (ctx.userLoggedIn === false) {
// The "Auth" part of the App. Segmented with it's own Navigation
return <Auth />;
} else {
// The "Main" part of the App. Segmented with it's own Navigation
return <Main />;
}
}
}}
</ConfigConsumer>
</ConfigProvider>
);
};
export default App;
|
Flow
- App loads,
isLoading
is true
by default, and shows SplashScreen
- GlobalProvider triggers
_getToken()
- Once Token, or lack there of, is fetched:
userToken
is set
userLoggedIn
is set based on if token was found
isLoading
is set to false
- User is redirected to
Auth
or Main
depending on userLoggedIn
GlobalContext functions, setToken
To finish this idea, this is how you would fetch functions out of the
GlobalContext.
Path: LoginScreen.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import * as React from "react";
import GlobalContext, { GlobalStates } from "GlobalContext";
import { Pressable, Text } from "react-native";
interface loginProps {
setToken: (token: string) => void,
username: string,
password: string,
}
function login({username, password, setToken}: loginProps){
try {
// do some api call
server_response = fetch();
if ("token" in server_response){
setToken(server_response["token"]);
}
} catch(error){
console.error(error);
}
}
// I'm sure this argument needs to be typed \/
export default function LoginScreen({ navigation }) {
const [usernameState, setUsernameState] = React.useState("test_user");
const [passwordState, setPasswordState] = React.useState("test_password");
// get setToken function (broken up to make eslint happy)
const context: GlobalStates = React.useContext(GlobalContext);
const setToken = context.setToken;
return (
<Pressable
onPress={() => login(usernameState, passwordState, setToken)}
>
<Text>Login</Text>
</Pressable>
)
}
|
Note
The WARNING: It must be accessed **inside** of a React Component
is displayed
in this block. You can not place the React.useContext
call inside of the
login
function. It might still work sometimes, but it’s not consistent,
and as your app grows this practice will lead to issues.
Conclusion
So far I am enjoying this solution and it has gotten zero complaints. If you’re
looking at useReducer
, useEffect
and useMemo
and are thinking “wow! That’s a
lot!” Trying giving this way a shot if you think it would be more intuitive.