React context has been there for a while. With the coming of React hooks, it's now much better. It has so many advantages, including the fact that the context API doesn't require any third-party libraries. We can use it in React apps to manage our state like redux.
In this article, we're going to manage our state with React context, to see by ourselves if it's better than redux regarding state's management.
Note: This article covers only the context API. We're going to build the same project with React context.
Prerequisites
To be able to follow along, you have to know at least the basics to advance features of React and particularly React hooks. A good grasp of redux can also help.
Setting Up the Project
If you're good to go, we can now create a new React app by running:
npx create-react-app react-context-hooks-example
Then, we have to create some files.
- Add a containers folder in the src, then create Articles.js file.
import React, { useState } from 'react';
import Article from '../components/Article/Article';
const Articles = () => {
const [articles, setArticles] = useState([
{ id: 1, title: 'post 1', body: 'Quisque cursus, metus vitae pharetra' },
{ id: 2, title: 'post 2', body: 'Quisque cursus, metus vitae pharetra' },
]);
return (
<div>
{articles.map((article) => (
<Article key={article.id} article={article} />
))}
</div>
);
};
export default Articles;
- Add a components folder in the src, then create AddArticle/AddArticle.js and Article/Article.js.
- In the Article.js
import React from 'react';
import './Article.css';
const article = ({ article }) => (
<div className="article">
{' '}
<h1>{article.title}</h1> <p>{article.body}</p>{' '}
</div>
);
export default article;
- In the AddArticle.js
import React, { useState } from 'react';
import './AddArticle.css';
const AddArticle = () => {
const [article, setArticle] = useState();
const handleArticleData = (e) => {
setArticle({
...article,
[e.target.id]: e.target.value,
});
};
const addNewArticle = (e) => {
e.preventDefault();
// The logic will come later
};
return (
<form onSubmit={addNewArticle} className="add-article">
<input
type="text"
id="title"
placeholder="Title"
onChange={handleArticleData}
/>
<input
type="text"
id="body"
placeholder="Body"
onChange={handleArticleData}
/>
<button>Add article</button>
</form>
);
};
export default AddArticle;
- In the App.js
import React, { Fragment } from 'react';
import Articles from './containers/Articles';
import AddArticle from './components/AddArticle/AddArticle';
function App() {
return (
<Fragment>
<AddArticle />
<Articles />
</Fragment>
);
}
export default App;
So, if you've done with all the instructions above, we can move on and start implementing the context API.
Create a context
A context helps us to handle state without passing down props on every component. Only the needed component will consume the context. To implement it, we need to create (it's optional) a new folder named context in our project, and add this code below to aricleContext.js.
- In context/aricleContext.js
import React, { createContext, useState } from 'react';
export const ArticleContext = createContext();
const ArticleProvider = ({ children }) => {
const [articles, setArticles] = useState([
{ id: 1, title: 'post 1', body: 'Quisque cursus, metus vitae pharetra' },
{ id: 2, title: 'post 2', body: 'Quisque cursus, metus vitae pharetra' },
]);
const saveArticle = (article) => {
const newArticle = {
id: Math.random(), // not really unique but it's just an example
title: article.title,
body: article.body,
};
setArticles([...articles, newArticle]);
};
return (
<ArticleContext.Provider value={{ articles, saveArticle }}>
{children}
</ArticleContext.Provider>
);
};
export default ArticleProvider;
The React library gives us access to a method called createContext. We can use it to as you might guess create a context. Here, we pass nothing to our context ArticleContext, but you can pass as argument object, array, string, etc. Then we define a function which will help us distribute the data through the Provider. We give to our Provider two values: the list of articles and the method to add an article. By the way, articles:articles and saveArticle:saveArticle is the same as articles and saveArticle it's just a convenient syntax in case it confuses you.
Now we've a context, however, we need to provide the context in order to consume it. To do that, we need to wrap our higher component with ArticleProvider and App.js might be the perfect one. So, let's add it to App.js.
Provide the context
- In App.js
import React from 'react';
import ArticleProvider from './context/articleContext';
import Articles from './containers/Articles';
import AddArticle from './components/AddArticle/AddArticle';
function App() {
return (
<ArticleProvider>
<AddArticle />
<Articles />
</ArticleProvider>
);
}
export default App;
As you see here, we first import our context provider ArticleProvider and wrap components which need to consume the context. Now what about consuming the context? and how we can do that. You might be surprised how easy it is to consume the context with hooks. So, let's do that.
Consume the context
We're going to consume the context in two components: Articles.js and AddArticle.js.
- In Articles.js
import React, { useContext } from "react"
import { ArticleContext } from "../context/articleContext"
import Article from "../components/Article/Article"
const Articles = () => {
const { articles } = useContext(ArticleContext) return (
<div>
{articles.map(article => (
<Article key={article.id} article={article} />
))}
</div>
)
}
export default Articles
With React hooks, we've now access to the useContext hook. And as you might guess, it will help us consume the context. By passing our context ArticleContext as argument to useContext, it gives us access to our state holden in articleContext.js. Here, we just need articles. Therefore, we pull it out and map through our articles and show them. Now, let's move on to AddArticle.js.
- In AddArticle.js
import React, { useState, useContext } from "react"import "./AddArticle.css"
import { ArticleContext } from "../../context/articleContext"
const AddArticle = () => {
const { saveArticle } = useContext(ArticleContext)
const [article, setArticle] = useState()
const handleArticleData = e => {
setArticle({
...article,
[e.target.id]: e.target.value,
})
}
const addNewArticle = e => {
e.preventDefault()
saveArticle(article)
}
return (
<form onSubmit={addNewArticle} className="add-article">
<input
type="text"
id="title"
placeholder="Title"
onChange={handleArticleData}
/>
<input
type="text"
id="body"
placeholder="Body"
onChange={handleArticleData}
/>
<button>Add article</button>
</form>
)
}
export default AddArticle
As the previous case, here again we use useContext to pull out saveArticle from our context. With that, we can now safely add a new article through the React Context.
We now manage our whole application's state through the React Context. However, we can still improve it through another hook named useReducer.
Enhance the context with useReducer
The useReducer hook is an alternative to useState. It's mostly use for the more complex state. useReducer accepts a reducer function with the initial state of our React app, and returns the current state, then dispatches a function.
This will be much more clear, when we start implementing it. Now, we've to create a new file reducer.js in our context folder and add this code block below.
- In reducer.js
export const reducer = (state, action) => {
switch (action.type) {
case 'ADD_ARTICLE':
return [
...state,
{
id: Math.random(), // not really unique but it's just an example
title: action.article.title,
body: action.article.body,
},
];
default:
return state;
}
};
As you can see, the function reducer receives two parameters: state and action. Then, we check if the type of action is equal to ADD_ARTICLE (you can create a constant or file to avoid mistyping), if it's the case add a new article to our state. This syntax might be familiar if you've used redux. Now, the logic to add a new article is handled by the reducer. We've not done yet, let's add it to our context file.
import React, { createContext, useReducer } from 'react';
import { reducer } from './reducer';
export const ArticleContext = createContext();
const ArticleProvider = ({ children }) => {
const [articles, dispatch] = useReducer(reducer, [
{ id: 1, title: 'post 1', body: 'Quisque cursus, metus vitae pharetra' },
{ id: 2, title: 'post 2', body: 'Quisque cursus, metus vitae pharetra' },
]);
return (
<ArticleContext.Provider value={{ articles, dispatch }}>
{children}
</ArticleContext.Provider>
);
};
export default ArticleProvider;
Here, we start by importing the useReducer hook and our function reducer. As i mention earlier, useReducer takes a function. Therefore, we've to pass our reducer function to it and as second argument the initial state of our application. Now useReducer gives us access to our articles and a dispatch function (you can name it whatever you like). And we can now, update our provider with these new values given by useReducer.
You can already see that our context file is now much cleaner. By renaming the function which add a new article to dispatch, we've now to update a little bit our AddArticle.js file.
- In AddArticle.js
import React, { useState, useContext } from 'react';
import './AddArticle.css';
import { ArticleContext } from '../../context/articleContext';
const AddArticle = () => {
const { dispatch } = useContext(ArticleContext);
const [article, setArticle] = useState();
const handleArticleData = (e) => {
setArticle({
...article,
[e.target.id]: e.target.value,
});
};
const addNewArticle = (e) => {
e.preventDefault();
dispatch({ type: 'ADD_ARTICLE', article });
};
return (
<form onSubmit={addNewArticle} className="add-article">
<input
type="text"
id="title"
placeholder="Title"
onChange={handleArticleData}
/>
<input
type="text"
id="body"
placeholder="Body"
onChange={handleArticleData}
/>
<button>Add article</button>
</form>
);
};
export default AddArticle;
Now, instead of pulling out saveArticle, now we get the dispatch function. It expects a type of action ADD_ARTICLE and a value article which will be the new article. With that, our project is now managed through the context API and React Hooks.
Redux VS the React Context: Who wins?
You can now clearly see the difference between Redux and React Context through their implementations on our project. However, Redux is far from dead or be killed by React Context. Redux is such boilerplate and require a bunch of libraries. But it remains a great solution towards props drilling.
The context Api with hooks is much more easier to implement and will not increase your bundle size.
However who wins? in my opinion, for a low-frequency updates like locale, theme changes, user authentication, etc. the React Context is perfectly fine. But with a more complex state which has a high-frequency updates, the React Context won't be a good solution. Because, the React Context will triggers a re-render on each update, and optimizing it manually can be really tough. And there, a solution like Redux is much easier to implement.
You can find the finished project here
Source: https://www.ibrahima-ndaw.com/blog/redux-vs-react-context-which-one-should-you-choose/