Use useReducer + useContext to replace Redux
Published: 2021-04-10 03:12:12

In React, it is troublesome to pass props, states, or stores down to or up to multiple nested components. The `react-redux` has been used by front-end developers for a long time to make global state management much easier. 

However, after React Hooks have been added to the React library in version 16.8, the combination use of `useContext` and `useReducer` provides developers a simpler solution to the global state management without the use of `react-redux`.

## ...But Why? Why do we use useReducer + useContext to replace Redux

The combination use of `useContext` and `useReducer` should look pretty familiar to you if you have used Redux before. But still, why using `useContext` + `useReducer` instead?

When there is no need for large combined reducers, Redux may be redundant and "overkill". With react hooks, we can manage global states without introducing additional dependencies.

Of course, Redux still has its advantage over more complicated situations, such as when needing multiple dispatch handlers for reducers. However, it is kind of a rare case. In most cases, React hooks provide a simpler and more reliable way (without introducing more dependencies) to reach the same purpose for global state management. 

## Intro to `useReducer` and `useContext`

`useReducer` and `useContext` are both hooks provided by React instead of outside libraries. 

According to [React Hooks API](https://reactjs.org/docs/hooks-reference.html):

`useReducer`: 

```javascript
const [state, dispatch] = useReducer(reducer, initialArg, init);
```

Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method. (If you’re familiar with Redux, you already know how this works.)

`useContext`: 

```javascript
const value = useContext(MyContext);
```

Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.

... Didn't get it? I know, API document takes time to sink in. In the following part, I would show a more concrete example of combining `useContext` and `useReducer` to manage global states.

## Time to apply `useContext` and `useReducer`

Imagine that we are writing the user authentication for our website. We would like to save whether the current user is authenticated or not, with the user token and the username if the user is indeed authenticated.

The authentication state can influence our app in many places and determine what information to show, so this can be a useful case as an example.

### Step 1: Organize your folders
Under `src` repository, create a new folder named `store`, and create two more folders named `actions` and `reducers` under it. As the names suggested, this `store` folder would contain main codes for our reducers and actions for these reducers.
![Screen Shot 2021-04-10 at 9.33.25 AM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7t330swvegpii7qdujrl.png)

### Step 2: Define context

Then, we need to save our context. Personally, I would like to create another folder named `utils` and create a context in it, but it doesn't matter much and depends on your preference.

In `utils/context.js`
```javascript
import React from "react";
const Context = React.createContext();
export default Context;
```

Very simple, uh? It seems that we didn't do much. We just created a new context. Well, let's continue.
 
### Step 2: Define action type constants
What actions do you need for user authentication? To simplify, I would imagine there are only two action types related: log in and log out. 

Create a file `action_type.js` under `action` folder, and define some constants for your actions. We define constants to reduce the chance of misspelling and also make our code more clear.

In `store/actions/action_type.js`:

```javascript
export const LOGIN = "LOGIN";
export const LOGOUT = "LOGOUT";
```

### Step 3: Create reducers
Now is the time to use the `useReducer` hook. Create a `authReducer.js` under `store/actions/reducers`.

In `store/actions/reducers/authReducer.js`:

```javascript
import * as ACTION_TYPES from "../actions/action_types";

export const initialState = {
  isAuth: false,
  username: "",
  token: "",
};

export const AuthReducer = (state = initialState, action) => {
  switch (action.type) {
    case ACTION_TYPES.LOGIN:
      return {
        ...state,
        isAuth: true,
        username: action.username,
        token: action.token,
      };
    case ACTION_TYPES.LOGOUT:
      return {
        ...state,
        isAuth: false,
        username: "",
        token: "",
      };
    default:
      return state;
  }
};

```

There are lots of things going on here. Let's look at each part. 

We firstly import the action constant from `store/actions/action_types.js`. As mentioned, it is a good practice to use constant variables instead of spelling the constant every time.

```javascript
import * as ACTION_TYPES from "../actions/action_types";
```

Then we define the initial states. In the initial, the user is not yet authenticated and thus has no token and username.

```javascript
export const initialState = {
  isAuth: false,
  username: "",
  token: "",
};
```
Finally, we define our reducer for user authentication. The reducer takes an initial state and an action object. The action object would specify what the type it is, and also payload to contains other useful information.

What should be returned is the current state after the action has been done. So, if the input action has a type of `LOGIN`, then that means the user is authenticated, so we return a new state with the `isAuth` set as true, and also with the corresponding `username` and `token` obtained from the input `action` object.

```javascript
export const AuthReducer = (state = initialState, action) => {
  switch (action.type) {
    case ACTION_TYPES.LOGIN:
      return {
        ...state,
        isAuth: true,
        username: action.username,
        token: action.token,
      };
    case ACTION_TYPES.LOGOUT:
      return {
        ...state,
        isAuth: false,
        username: "",
        token: "",
      };
    default:
      return state;
  }
};
```

### Step 3: Define actions
We want to add some further abstraction to our auth reducers. We know that reducers update their current state based on the input actions. An input action is an object that defines the type of action as well as other needed data.

We can thus use pre-defined data returned from pre-defined functions to represent actions.

Create a file named `actions.js` in `store/actions`.

In `store/actions/actions.js`:

```javascript
export const login = (data) => {
  return {
    type: ACTION_TYPES.LOGIN,
    username: data.username,
    token: data.token,
  };
};

export const logout = () => {
  return {
    type: ACTION_TYPES.LOGOUT,
  };
};
```
In this case, we can directly use the data returned from these actions to pass into a reducer, instead of defining the whole action objects each time.

### Step 4: Save states and handlers of reducers into a global context

Now we finally get back to the context that we created at the first beginning. How do we use it?

Create a new file named `context_state_config.js` in `src`. 

```javascript
import React, { useReducer } from "react";
import Context from "./utils/context";
import * as ACTIONS from "./store/actions/actions";
import * as AuthReducer from "./store/reducers/auth_reducer";
import App from "./App";

const ContextState = () => {
  const [stateAuthReducer, dispatchAuthReducer] = useReducer(
    AuthReducer.AuthReducer,
    AuthReducer.initialState
  );

  const handleLogin = (data) => {
    dispatchAuthReducer(ACTIONS.login(data));
  };

  const handleLogout = () => {
    dispatchAuthReducer(ACTIONS.logout());
  };

  return (
    <Context.Provider
      value={{
        authState: stateAuthReducer.isAuth,
        usernameState: stateAuthReducer.username,
        tokenState: stateAuthReducer.token,
        handleUserLogin: (username) => handleLogin(username),
        handleUserLogout: () => handleLogout(),
      }}
    >
      <App />
    </Context.Provider>
  );
};

export default ContextState;
```

Once again, let's go through each part.

Import the context, actions, and our auth reducer. Also import the component that context should be provided to. Is is <App /> in our example.
```javascript
import React, { useReducer } from "react";
import Context from "./utils/context";
import * as ACTIONS from "./store/actions/actions";
import * as AuthReducer from "./store/reducers/auth_reducer";
import App from "./App";
```
Use [context name].Provider as a context provider, to wrap around <App /> that needs global state, and export the returned component. 
```javascript
const ContextState = () => {
  return (
    <Context.Provider>
      <App />
    </Context.Provider>
  );
};

export default ContextState;
```
However, the global states are not saved into the context provider yet. We want to save the states and handlers for updating the states into the global context.

According to React hooks documentation, `userReducer` returns the current state paired with a dispatch function. So, we can get access to the current states in auth reducer by `stateAuthReducer`, and update the states by `dispatchAuthReducer`.

Here, we also add further abstraction to the dispatch functions. Instead of using dispatchers of the auth reducer, we can define our handlers to make it even easier to understand and pass them into the global context.

```javascript
  const [stateAuthReducer, dispatchAuthReducer] = useReducer(
    AuthReducer.AuthReducer,
    AuthReducer.initialState
  );

  const handleLogin = (data) => {
    dispatchAuthReducer(ACTIONS.login_success(data));
  };

  const handleLogout = () => {
    dispatchAuthReducer(ACTIONS.logout());
  };
```
And finally, we save states and handlers into our global context, for the nested components to get direct access to.
```javascript
  return (
    <Context.Provider
      value={{
        authState: stateAuthReducer.isAuth,
        usernameState: stateAuthReducer.username,
        tokenState: stateAuthReducer.token,
        handleUserLogin: (username) => handleLogin(username),
        handleUserLogout: () => handleLogout(),
      }}
    >
      <App />
    </Context.Provider>
  );
```

Since we export `<App />` with <Context.Provider> wrapped around, now the `<ContextState />` component is an app component with all the states and handlers we need for user authentication.

### Example of using and changing auth states from the global context

Finally, ley's look at how we get access to current user auth states.

```javascript
import React, { useContext } from "react";
import Context from "../utils/context";

export default function Example() {
  const context = useContext(Context);

  return (
    <div>
      {context.isAuth ? (
        <p>{"Hi, " + context.username}</p>
      ) : (
        <p>Please log in</p>
      )}
    </div>
  );
}
```

And we can use any of the handlers we pass as values into <Context.Provider> to update the user authentication states.

Thanks for reading!