Context in React is used to share data that is global to a component tree such as an authenticated user or preferred theme without passing that data as props. Here we won’t be discussing the basics of React Context API but instead we will see how we can use it elegantly.
function App(props) {
const [number, setNumber] = useState(0);
const changeNumberHandler = () => {
setNumber(c => c + 1)
}
return (
<div className="App">
The Main App Component {number}
<UserProvider> {/* Our Custom Provider */}
<Comp1 />
</UserProvider>
<button onClick={changeNumberHandler}>
Changing Main Component Number
</button>
</div>
);
}
// comp1 renders
<Comp2 />
// ... which renders ...
<Comp3 />
<Comp4 />
Here we will be using [https://jsonplaceholder.typicode.com/users](https://jsonplaceholder.typicode.com/users)/{userId}
to fetch the user of userId
. We will use Math.random()
to generate a random number from 1 to 10 which we will use as userId
. After fetching the result, we will provide that result to Comp3
and Comp4
. Along with this, we will also provide a function that will change userId
and fetch the result for that new userId
.
Creates a new context object that is used to pass down the value to a component tree without using props.
const MyContext = React.createContext(defaultValue);
Provider allows the consuming React component to subscribe to the context changes.
<MyContext.Provider value={someValue}>
All the consumers that are descendants of Provider will re-render whenever the value prop changes.
It can be used in functional components which accepts a context object and returns the current context value.
const value = useContext(MyContext);
We will create one custom provider (UserProvider
) which will be used to provide the value down to every descendant. To consume that value, we will create one custom Hook (useUser
).
import React, {
useState,
useEffect,
useContext,
} from 'react'
import axios from 'axios'
const UserContext = React.createContext(undefined);
const UserProvider = ({
children
}) => {
const [user, setUser] = useState(null);
const [random, setRandom] = useState(1);
useEffect(() => {
const url = `https://jsonplaceholder.typicode.com/users/${random}`;
axios.get(url).then(res => {
setUser(res.data);
}).catch(err => {
console.error(err)
});
}, [random]);
const changeUser = () => {
const randomNumber = Math.floor(Math.random() * 10 + 1);
setRandom(randomNumber);
}
const data = [
user,
changeUser
]
return (
<UserContext.Provider value={data}>
{children}
</UserContext.Provider>
)
}
const useUser = () => {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser can only be used inside UserProvider');
}
return context;
}
export {
UserProvider,
useUser
}
user1.js
Here we have given undefined
as the default value to the context. This default value, in general, is used whenever we try to use the value of context objects in components that are not the decendants of the provider of this context object.
We are giving an array to the value
prop of UserContext.Provider
. This array has the value of user
state and a function changeUser
that is calculating a random number from 1 to 10 and storing it in a state callled random
.
We are making an Ajax call to our endpoint in the useEffect
Hook, which has only random
state in its dependency array. That means this effect will only execute when our random
state changes.
In the return statement, we are writing the main context provider logic, which will pass down the values to all consumers which are its descendants.
<UserContext.Provider value={data}>
{children}
</UserContext.Provider>
After our custom provider, we have created our custom Hook. In this Hook, first we are using a useContext
Hook to get the value of context object. After that, to prevent accidental usage of our custom Hook in any non-descendant component, we are checking whether the value obtained using useContext
is equal to our defaultValue
given during the creation of our context object. In our case, this default value is undefined
. If the value is undefined
, then we can say that our custom Hook is used in any non-descendant component.
//undefined is given as default value
const UserContext = React.createContext(undefined);
Then, from our custom Hook, we return the obtained context value.
We know that all descendant components of Provider will re-render as the value prop changes and also they cannot bail out of the updating even though they have used PureComponent
, shouldComponentUpdate
or React.memo
.
Also, there could be some unintentional renders in consumers when a provider’s parent re-renders.
Let’s use React.memo
in Comp1 so that no unintentional renders happen in Comp3 and its children due to changes in App component.
import React from 'react';
import Comp2 from './comp2';
const Comp1 = () => (
<div className="comp1">
Comp1
<Comp2 />
</div>
)
export default React.memo(Comp1);
Comp1.js
import React, {useState} from 'react';
import Comp1 from './comp1';
import { UserProvider } from './user'
import './App.css';
function App() {
const [number, setNumber] = useState(0);
const changeNumberHandler = () => {
setNumber(c => c + 1)
}
return (
<div className="App">
The Main App Component {number}
<UserProvider> {/* Our Custom Provider */}
<Comp1 />
</UserProvider>
<button onClick={changeNumberHandler}>Changing Main Component Number</button>
</div>
);
}
export default App;
appContext.js
App component has one button. Clicking on it updates the number
state.
Now we can observe in the above image that Comp3 and Comp4 are being re-rendered even though we have used React.memo
in Comp1, while Comp2 is not being re-rendered.
This is happening because we are using an array as the value prop in our Provider.
const data = [user, changeUser]
return (
<UserContext.Provider value={data}>
{children}
</UserContext.Provider>
)
So whenever App component gets re-rendered, our UserProvider
component also gets re-rendered. That will create the new reference for our changeUser
function as well as for our data
array, which will trigger the re-render of all consumers.
To overcome this, we will use useCallback
for our changeUser
function and useMemo
for our data
array.
const changeUser = useCallback(() => {
const randomNumber = Math.floor(Math.random() * 10 + 1);
setRandom(randomNumber);
}, [])
const data = useMemo(() => ([
user,
changeUser
]), [user, changeUser])
import React, {
useState,
useEffect,
useContext,
useCallback,
useMemo
} from 'react'
import axios from 'axios'
const UserContext = React.createContext(undefined);
const UserProvider = ({
children
}) => {
const [user, setUser] = useState(null);
const [random, setRandom] = useState(1);
useEffect(() => {
const url = `https://jsonplaceholder.typicode.com/users/${random}`;
axios.get(url).then(res => {
setUser(res.data);
}).catch(err => {
console.error(err)
});
}, [random]);
const changeUser = useCallback(() => {
const randomNumber = Math.floor(Math.random() * 10 + 1);
setRandom(randomNumber);
}, [])
const data = useMemo(() => ([
user,
changeUser
]), [user, changeUser])
return (
<UserContext.Provider value={data}>
{children}
</UserContext.Provider>
)
}
const useUser = () => {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser can only be used inside UserProvider');
}
return context;
}
export {
UserProvider,
useUser
}
user.js
import React from 'react';
import { useUser } from './user';
const Comp3 = () => {
const [user] = useUser();
return (
<div className="comp3">
Comp3
<hr />
<p>{user && user.id}</p>
<p>{user && user.name}</p>
<p>{user && user.website}</p>
</div>
)
}
export default Comp3;
comp3.js
import React from 'react';
import { useUser } from './user';
const Comp4 = () => {
const [user, changeUser] = useUser();
return (
<div className="comp3">
Comp4
<hr />
<p>{user && user.id}</p>
<p>{user && user.name}</p>
<p>{user && user.website}</p>
<button onClick={changeUser}>Change User</button>
</div>
)
}
export default Comp4;
comp4.js
import React from 'react';
import Comp3 from './comp3';
import Comp4 from './comp4';
const Comp2 = () => (
<div className="comp2">
Comp2
<div className="compContainer">
<Comp3 />
<Comp4 />
</div>
</div>
)
export default Comp2;
comp2.js
Comp3 and Comp4 are using the useUser
Hook. Also, Comp4 is calling the changeUser
function when the button is clicked. When changeUser
is called, the data in both Comp3 and Comp4 get changed. This is all possible because of React Context.
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Learn JavaScript - Become a Zero to Hero
☞ Javascript Project Tutorial: Budget App
☞ JavaScript for React Developers | Mosh
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch