DEV Community

Kexin Lin
Kexin Lin

Posted on • Updated on

Use useReducer + useContext to replace Redux

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:

useReducer:

const [state, dispatch] = useReducer(reducer, initialArg, init);
Enter fullscreen mode Exit fullscreen mode

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:

const value = useContext(MyContext);
Enter fullscreen mode Exit fullscreen mode

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 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

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

import React from "react";
const Context = React.createContext();
export default Context;
Enter fullscreen mode Exit fullscreen mode

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:

export const LOGIN = "LOGIN";
export const LOGOUT = "LOGOUT";
Enter fullscreen mode Exit fullscreen mode

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:

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;
  }
};

Enter fullscreen mode Exit fullscreen mode

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.

import * as ACTION_TYPES from "../actions/action_types";
Enter fullscreen mode Exit fullscreen mode

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

export const initialState = {
  isAuth: false,
  username: "",
  token: "",
};
Enter fullscreen mode Exit fullscreen mode

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.

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;
  }
};
Enter fullscreen mode Exit fullscreen mode

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:

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

export const logout = () => {
  return {
    type: ACTION_TYPES.LOGOUT,
  };
};
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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 in our example.

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";
Enter fullscreen mode Exit fullscreen mode

Use [context name].Provider as a context provider, to wrap around that needs global state, and export the returned component.

const ContextState = () => {
  return (
    <Context.Provider>
      <App />
    </Context.Provider>
  );
};

export default ContextState;
Enter fullscreen mode Exit fullscreen mode

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.

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

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

  const handleLogout = () => {
    dispatchAuthReducer(ACTIONS.logout());
  };
Enter fullscreen mode Exit fullscreen mode

And finally, we save states and handlers into our global context, for the nested components to get direct access to.

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

Since we export <App /> with 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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

Thanks for reading!

Top comments (3)

Collapse
 
lalami profile image
Salah Eddine Lalami

@ IDURAR , we use react context api for all UI parts , and we keep our data layer inside redux .

Here Article about : πŸš€ Mastering Advanced Complex React useContext with useReducer ⭐ (Redux like Style) ⭐ : dev.to/idurar/mastering-advanced-c...


Mastering Advanced Complex React useContext with useReducer (Redux like Style)

Collapse
 
haiqwu profile image
Haiqi Wu • Edited

Hey, good work!
Wonder that if we can add API calls into it without things like rredux-promise-middleware inside of action:

export const login = async (data) => {
try {
const resp = await axios.post('reqres111.in222/api/loginUser', userInfo);

return {
type: ACTION_TYPES.LOGIN,
username: data.username,
token: data.token,
error: false,
resp: resp,
};
} catch(err) {
return {
type: ACTION_TYPES.LOGIN,
username: data.username,
token: data.token,
error: true,
};

}

};

Collapse
 
t0nghe profile image
Tonghe Wang

Good work. Thanks for sharing!