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.
Context API with useReducer
This is a demo app to show how Context API x useReducer work ๐ง๐ปโโ๏ธ
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;
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.
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>
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)
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.
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.This is a big merit for me to use Context API instead of Redux.
That's why you can replace Redux with Context API with ease.
Thanks.
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.
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.
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):
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 theuseContext(context)
consumers whenever the provider is rendered. So, plz take a look atuseMemo
;-)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.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!This. Context is fine for small isolated use cases, but any mid to large app is guaranteed to suffer performance issues.