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:

  1. Getting/Setting/Removing authentication token on device’s storage
  2. Store if the user is logged in (has token)
  3. 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

  1. App loads, isLoading is true by default, and shows SplashScreen
  2. GlobalProvider triggers _getToken()
  3. 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
  1. 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.

comments powered by Disqus