DEV Community

Cover image for How to manage global data with Context API, without Redux on React
Taishi
Taishi

Posted on • Edited on • Originally published at en.taishikato.com

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

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

const [user, setUser] = useState(null);
Enter fullscreen mode Exit fullscreen mode

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

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

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.

Context API with useReducer

This is a demo app to show how Context API x useReducer work 🧝🏻‍♀️

Screen Shot 2563-10-03 at 18 04 06

1. Set your firebase project

Please edit ./src/firebase.js.

2. yarn start

That's it!






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

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

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

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

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

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

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.

Top comments (7)

Collapse
 
leob profile image
leob • Edited

By now I've seen many articles saying "you don't need Redux, use Context (with useReducer and so on). Prove me that it's simpler with Context than with Redux, and that you need way less boilerplate - because I genuinely don't see it!

It's (a bit) different but not necessarily better, the only objective advantage is you need one or two fewer dependencies in your package.json. Conceptually it's almost the same and much of the code is also largely the same, only a few pieces of syntax differ.

Collapse
 
taishi profile image
Taishi

Prove me that it's simpler with Context than with Redux

I have never said it's simpler than Redux, but 1. Use state variable can be an easier and better way for a small app to manage data globally.

the only objective advantage is you need one or two fewer dependencies in your package.json

This is a big merit for me to use Context API instead of Redux.

much of the code is also largely the same

That's why you can replace Redux with Context API with ease.

Thanks.

Collapse
 
faiwer profile image
Stepan Zubashev

That's why you can replace Redux with Context API with ease

If you want to replace Redux with your own solution using context API (btw redux uses it too under the hood), you need to implement much more code than you did in this article. Otherwise your solution would be extremely slow at big projects.

Thread Thread
 
leob profile image
leob • Edited

That's also what I fear, having read about that in other articles - you can get unnecessary re-renders and so on unless you implement further optimizations - by then you'd start wondering if you threw out the baby with the bath water.

Collapse
 
faiwer profile image
Stepan Zubashev

Thx, for the article. But there's 2 things you probably should to worry about (especially if you want to use it in production for big applications):

  1. When you write code like <Provider value={{ something }}> you're creating a performance issue. Because { something } is always a new object. It automatically rerenders all the useContext(context) consumers whenever the provider is rendered. So, plz take a look at useMemo ;-)

  2. There's a reason why no mature solutions use React context for stored values. They use it only to keep the store itself (that's the same object always). Because otherwise any change of the store will rerender all the children that use the context. I highly recommend you to take a look at useSelector code in react-redux. Or at any other similar solution. At now there's no simple way to avoid using own-implemented observable model, to make a performant solution. Context API is too simple to do it properly. But I hope soon we will seen something that solves the problem.

Collapse
 
taishi profile image
Taishi

Thank you, Stepan!

Ok, so there is an issue from a performance perspective.
That is what I didn't know.

Yeah, useMemo is good for optimizing performance 👍

I will take a look at useSelector code!

Collapse
 
jamesthomson profile image
James Thomson

This. Context is fine for small isolated use cases, but any mid to large app is guaranteed to suffer performance issues.