OOP React Native Auth Flow
Edit: for the love of god don’t do this, Use Redux; take the
upfront hit and don’t be lazy like me! The core principles of the
auth flow still work, but if you expect your app to get somewhat
large stay away from context- and don’t build the “core” around it as
you’ll run into refreshing problems
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 Stack Overflow
Post: React New Context API - Access Existing Context across Multiple Files
I also like how it’s more of an OOP approach, but this might looked down upon
in the React “normal way of doing it” crowd; you are 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 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
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.
Edit: markdownlint, textlint and meta TOML -> YAML