How to manage global data with Context API, without Redux on React

Hi, Taishi here 👋

How do you manage global data on React? I used to use Redux for that, however, I currently use Context API for that purpose and I don’t even install redux and redux-related packages!

2 ways to implement it with Context API

I think there are 2 ways to make it happen. A simple and complicated one.

I am gonna explain the simple one first ☝️

Imagine we want to manage the logged-in user data.

1. Use state variable

First of all, we need a Context component for sure.

I found this way when I was reading next.js/userContext.js at master · vercel/next.js 😋

Add userContext.js

Let’s make ./src/context/userContext.js.

// File: ./src/context/userContext.js
import React, { useState, useEffect, createContext, useContext } from 'react';
import firebase from '../firebase/clientApp';

export const UserContext = createContext();

export default function UserContextComp({ children }) {
  const [user, setUser] = useState(null);
  const [loadingUser, setLoadingUser] = useState(true); // Helpful, to update the UI accordingly.

  useEffect(() => {
    // Listen authenticated user
    const unsubscriber = firebase.auth().onAuthStateChanged(async (user) => {
      try {
        if (user) {
          // User is signed in.
          const { uid, displayName, email, photoURL } = user;
          // You could also look for the user doc in your Firestore (if you have one):
          // const userDoc = await firebase.firestore().doc(`users/${uid}`).get()
          setUser({ uid, displayName, email, photoURL });
        } else setUser(null);
      } catch (error) {
        // Most probably a connection error. Handle appropriately.
      } finally {
        setLoadingUser(false);
      }
    });

    // Unsubscribe auth listener on unmount
    return () => unsubscriber();
  }, []);

  return (
    <UserContext.Provider value={{ user, setUser, loadingUser }}>
      {children}
    </UserContext.Provider>
  );
}

// Custom hook that shorhands the context!
export const useUser = () => useContext(UserContext);

As you can see, UserContextComp component has user state variable.

const [user, setUser] = useState(null);

We store the user data in this user variable and update it with setUser() function.

Edit index.js

Now we have to use the UserContextComp component to consume it! Edit ./src/index.js like below.

// File: ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import UserProvider from './context/userContext';

ReactDOM.render(
  <React.StrictMode>
    <UserProvider>
      <App />
    </UserProvider>
  </React.StrictMode>,
  document.getElementById('root'),
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Now we can use user variable and update it with setuser() function everywhere ✌️

How to consume it

Import the useUser function from the ./src/context/userContext.js and get the variable you need. In this case, we take loadingUser, user, and setUser.

import React from 'react';
import { useUser } from '../context/userContext';

const MyComponent = () => {
  const { loadingUser, user, setUser } = useUser();

  return (
    <>
      {loadingUser ? (
        <div>loading…</div>
      ) : (
        <div>Welcome, {user.displayName}</div>
      )}
    </>
  );
};

export default MyComponent;

Please just use setUser if you need to update the user data just like when you update the usual state variable.

2. Use dispatch and reducer (more Redux way)

In this way, we use useContext with useReducer hook.

I feel like this way is Redux without Redux 🤤 Sure, Redux uses Context API inside itself.

By the way. I made an example app here. Please take a look at this if you wanna make it happen on your local environment. {% github taishikato/context-api-with-useReducer %}

Anyway, let’s dive into it!

Add ./src/context/reducer.js

If you are familiar with Redux, you can understand this with ease.

Now we are gonna define the reducer function and initialState. The default value of user is null.

// File: ./src/context/reducer.js
export const initialState = {
  user: null,
};

export const actionTypes = {
  SET_USER: 'SET_USER',
};

const reducer = (state, action) => {
  switch (action.type) {
    case actionTypes.SET_USER:
      return {
        ...state,
        user: action.user,
      };
    default:
      return state;
  }
};

export default reducer;

Make ./src/context/StateProvider.js

// File: ./src/context/StateProvider.js`

import React, { createContext, useContext, useReducer } from 'react';

export const StateContext = createContext([]);

export const StateProvider = ({ reducer, initialState, children }) => (
  <StateContext.Provider value={useReducer(reducer, initialState)}>
    {children}
  </StateContext.Provider>
);

export const useStateValue = () => useContext(StateContext);

Set the Provider in ./src/index.js

Because of this, we can consume the StateContext component everywhere!

// File: ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
+ import { StateProvider } from './context/StateProvider';
+ import reducer, { initialState } from './context/reducer';

ReactDOM.render(
  <React.StrictMode>
+    <StateProvider initialState={initialState} reducer={reducer}>
      <App />
+    </StateProvider>
  </React.StrictMode>,
  document.getElementById('root'),
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Now display the logged in user’s name!

Make a Auth component and use it in App.js like below.

We need login/logout methods (handleLogin, handleLogout) to handle onclick events, so make them as well.

// File: ./src/App.js
import React from 'react';

import Auth from './Auth';
import { auth, provider } from './firebase';
import { useStateValue } from './context/StateProvider';
import { actionTypes } from './context/reducer';
import './App.css';

function App() {
  const [state, dispatch] = useStateValue();

  const handleLogin = async () => {
    try {
      const result = await auth.signInWithPopup(provider);
      dispatch({
        type: actionTypes.SET_USER,
        user: result.user,
      });
    } catch (err) {
      alert(err.message);
    }
  };

  const handleLogout = async () => {
    await auth.signOut();
    dispatch({
      type: actionTypes.SET_USER,
      user: null,
    });
  };

  return (
    <Auth>
      <div className="App">
        <header className="App-header">
          <div>{state.user?.displayName}</div>
          {state.user ? (
            <button onClick={handleLogout}>Logout</button>
          ) : (
            <button onClick={handleLogin}>Login</button>
          )}
        </header>
      </div>
    </Auth>
  );
}

export default App;

As the reference says, useReducer returns state and dispatch.

An alternative to useState. 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.)

That’s why we can get variables like this.

useStateValue() returns useContext(StateContext), and this returns useReducer(reducer, initialState).

const [state, dispatch] = useStateValue();

Now you see like this and you can login/logout.

Login

If you logged in successfully, you can see your name as below.

When the value of state.user is set, your name will be shown.

<div>{state.user?.displayName}</div>

Logout

Note

I think 2. Use dispatch and reducer (more Redux way) may look complicated to you, but we can easily understand what kind of data this app manages globally in initialState. In this example, we manage only user variable globally, but imagine if we manage like 10 variables 😅

Hope this helps.