Since react announced its support for hooks in a stable version of react, I haven't created any new class based components. A lot of us still use redux to manage our state but the truth is we do not always need this extra layer of complexity. With react's context API you can also share data across various components.
I assume you already have at least a little knowledge of react, react-hooks and redux. If you have no knowledge of redux, no problem, you can skip directly to using context.
Let's say we have an authentication data that contains our login status and user details, and also data containing a list of articles that we will like to display at various parts of our app. If we are using redux, we will be able to share this data across various components in our app using a concept commonly known as 'mapStateToProps' by connecting our component with the redux store.
import react from 'react';
import connect from 'react-redux';
function User(props) {
const getUserArticles = (articles) => articles
.filter(article => article.userId === props.user.id);
return (
<div>
<h1>{`${props.user.name}'s Article`}</h1>
<ul>
{getUserArticles(props.articles)
.map(article => <li key={article.id}>{article.title}</li>)}
</ul>
</div>
);
}
const mapStateToProps = ({auth, article}) => ({
user: auth.user,
articles: article.articles
});
export default connect(mapStateToProps)(User);
That's a typical example of what our component could look like if we managed the auth and article states via redux. Our articles and authentication data is being fetched by our service behind the scene and dispatched to our redux state. This state is now accessible by any component the same way we've done above.
Using Context
We can achieve this same data sharing across components with context. Creating a context in react looks like:
const MyContext = React.createContext(defaultValue);
createContext
takes default value as a parameter, which is only used if no matching provider is found above the consuming component's tree.
Let us create the article and authentication contexts.
import React from "react";
export const authContext = React.createContext({
loggedIn: false,
user: {},
updateAuth: () => {}
});
export const articleContext = React.createContext({
articles: [],
updateArticles: () => {}
});
Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.
<MyContext.Provider value={/* some value */}>
Any functional Component can read a context and subscribe to its changes by using the hook
useContext
const value = useContext(MyContext)
According to the react docs, useContext
accepts a context object (the value returned from React.createContext) and returns the current context value for that context.
All Components that are descendants of a Provider will re-render whenever the Provider’s value prop changes if they are subscribed to that provider.
Let's see how we can make use of the authContext and the articleContext we defined earlier.
import React, { useState } from "react";
import { authContext, articleContext } from "./contexts";
import UserComponent from "./user";
function App() {
const [auth, setAuth] = useState({
loggedIn: false,
user: {},
updateAuth: update => setAuth(auth => ({ ...auth, ...update }))
});
const [articles, setArticles] = useState({
articles: [],
updateArticles: articles => setArticles(articles)
});
return (
<authContext.Provider value={auth}>
<articleContext.Provider value={articles}>
<UserComponent />
</articleContext.Provider>
</authContext.Provider>
);
}
export default App;
At this point you probably have two questions;
- Why not pass the value directly into the value prop for the providers?
- Why are we defining another update method when the state hook already returns an update method?
For the first question, there is a caveat that is stated in the react doc. it says: Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value:
function App() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
To get around this, lift the value into the parent’s state.
For the second question, we need to make the update function really easy to use, so that the user only worries about the current value property they are trying to update without overwriting or removing the unchanged properties. Our update functions merge the new values with the old ones using a spread operator.
When using redux, we dispatch actions to update our redux state. A typical action dispatch will be done this way:
store.dispatch({type: 'update_articles', value: articles })
And we go ahead to use the dispatched actions in our reducer by doing something like
export const articlesreducer = (state = {}, action) => {
switch(action.type) {
case('update_articles'):
return { ...state, articles: action.value };
default:
return state;
}
}
That was a typical example of how we would update our redux state. With Context, we can do away with all of that. If you've never used redux you probably didn't need to see that, my apologies
Now we are going to refactor our user component and mock a promise based service that contains functions for fetching auth and articles data.
Here is what our mocked service could look like:
export const getAuth = () => {
return new Promise(resolve => {
resolve({
loggedIn: true,
user: {
name: "Jon Doe",
id: "1"
}
});
});
};
export const getArticles = () => {
return new Promise(resolve => {
resolve([
{ id: "1", userId: "1", title: "A few good men" },
{ id: "2", userId: "1", title: "Two and a half guns" },
{ id: "3", userId: "1", title: "Hey brother" }
]);
});
};
We can now refactor our user component from the one connected to a redux store to this one that subscribes to the context providers:
import React, { useContext, useEffect } from "react";
import { authContext, articleContext } from "./contexts";
import { getAuth, getArticles } from "./services";
function User() {
const { articles, updateArticles } = useContext(articleContext);
const auth = useContext(authContext);
useEffect(() => {
getAuth().then(data => auth.updateAuth(data));
getArticles().then(articles => updateArticles({ articles }));
}, [auth.updateAuth, updateArticles]);
const getUserArticles = articles =>
articles.filter(article => article.userId === auth.user.id);
return (
<div>
<h1>{`${auth.user.name}'s Article`}</h1>
<ul>
{getUserArticles(articles).map(article => (
<li key={article.id}>{article.title}</li>
))}
</ul>
</div>
);
}
export default User;
The user component now reads values from the article and auth contexts and rerenders if there is a change in the value of either of these. We can also update the contexts from the user component.
This form of data management might seem like an overkill for this little project we have created but the purpose of this is just to see how we can use react's context API in place of redux. This will be ideal in a larger application where various components with different levels of nesting need access to the auth and articles data.
I will advise you head over to the official react documentation site to learn more about the context API. For more reference, the code in this article lives in this codesandbox
Top comments (5)
Thank you for sharing great example, I tried running sandbox and got the error:
←→1 of 3 errors on the page
TypeError
updateArticles is not a function
8 |
9 | useEffect(() => {
10 | getAuth().then(data => auth.updateAuth(data));
Redux R.I.P.
How would you change article with for example, id 2?
Great explanation and example. I never used Redux but I use MobX heavily. This looks like a welcome replacement. I’ll refactor a small project to use and reply with any observations. Thanks
Looking forward to that