Originally posted on my blog
React is awesome, we can't say it enough. But where it comes to the state management part, things become tricky. There is so much terminology to retain: state, store, actions, reducers, middleware, etc. With medium size or bigger react apps, managing our state can be really hard as our application grows. We need to manage it either by redux or alternatives like the context API, flux, etc. In this article, we will focus on redux and how it works with React. Redux is a stand-alone library, it's framework agnostic, that's mean you can use it with other frameworks or just vanilla JavaScript.
In this post, I will lead you through 7 steps to understand react-redux in the easiest way.
- Prerequisite
- 1. What is a state?
- 2. What is redux and why we need it?
- 3. What is a reducer?
- 4. What is a store?
- 5. How to connect our store to React?
- 6. What is an action?
- 7. How to handle asynchronous code with redux?
- Conclusion
- Resources
Prerequisite
This post assumes that you have at least a basic to a mid-level understanding of React and ES6. Then, you'll need to create a fresh react app with this command:
npx create-react-app react-redux-example
And add to your react app the redux
and react-redux
packages by running in your shell
npm install redux react-redux
Then, we need to create some files.
- Add a
containers
folder in thesrc
, then createArticles.js
file.
import React, { useState } from "react"
import Article from "../components/Article/Article"
import AddArticle from "../components/AddArticle/AddArticle"
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" },
])
const saveArticle = e => {
e.preventDefault()
// the logic will be updated later
}
return (
<div>
<AddArticle saveArticle={saveArticle} />
{articles.map(article => (
<Article key={article.id} article={article} />
))}
</div>
)
}
export default Articles
- Add a
components
folder in thesrc
, then createAddArticle/AddArticle.js
andArticle/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 = ({ saveArticle }) => {
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
- In the
App.js
import React from "react"
import Articles from "./containers/Articles"
function App() {
return <Articles />
}
export default App
So, if you've done with the prerequisite, we can move on and demystify what is a state.
1. What is a state?
The heart of every react stateful component is its state. It determines how the component should render or behave. To really understand the state, we must apply it to real examples. Is the user authenticated? is a state that controls if a user is authenticated or not, is modal open? is also a state which looks if a given modal is open or not the same as a list of articles or a counter etc.
// Class based component
state = {
articles: [
{ id: 1, title: "post 1", body: "Quisque cursus, metus vitae pharetra" },
{ id: 2, title: "post 2", body: "Quisque cursus, metus vitae pharetra" },
],
}
// React hooks
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" },
])
Now we know what is a state, it's time to introduce redux and dive deeper into it.
2. What is redux and why we need it?
Managing our state without redux or alternatives can be tough. Imagine we have to check on every component if the user is authenticated or not. To handle that use-case, we have to pass props through every component and following the application growth, it's just impossible to manage our state like that. And there is where redux really shines.
Redux is an independent library that helps us manage our state by giving access to our components the state it needs via a central store. Redux stores the whole state of our app in an immutable object tree.
Another broad term: store, to understand it well we first need to explain what is a reducer?
3. What is a reducer?
A reducer is a pure function that receives the old (previous) state and an action as arguments, then returns as output the updated state. The reducer handles only synchronous code, that's mean no side effect like HTTP request or anything like that. We can still handle asynchronous code with redux and we'll learn how to do it later. By the way, if you get confused by the term action, no worries, it will be much clearer later. So, Let's create our very first reducer.
The structure of your files is totally up to you, however, I'll follow the convention and create a store
folder in the project to hold our reducers, actions, etc. Then, create a reducer.js
file.
- In
reducer.js
const initialState = {
articles: [
{ id: 1, title: "post 1", body: "Quisque cursus, metus vitae pharetra" },
{ id: 2, title: "post 2", body: "Quisque cursus, metus vitae pharetra" },
],
}
const reducer = (state = initialState, action) => {
return state
}
export default reducer
As I said earlier, a reducer is just a function that receives the previous state and an action as parameters and returns the updated state. Here, we have not a previous state, so it will be undefined, therefore we need to initialize it with initialState
which hold our predefined articles.
Now we've set up our reducer, it's time to create our store
4. What is a store?
A store holds the whole state tree of our react app. It's where our application state live. You can see it as a big JavaScript object. To create a store we need a reducer to pass as an argument. We already have a reducer, let's connect it to our store.
- In our
index.js
file.
import React from "react"
import ReactDOM from "react-dom"
import { createStore } from "redux"
import "./index.css"
import App from "./App"
import reducer from "./store/reducer"
const store = createStore(reducer)
ReactDOM.render(<App />, document.getElementById("root"))
To create a store, we first need to import createStore
from the redux package, then import our reducer and finally pass it as an argument to the store createStore(reducer)
. With that, we successfully create our store, but we've not done yet, we have to connect it to our react app.
5. How to connect our store to React?
To connect the store to react, we need to import a helper function named Provider
from the react-redux package. Then wrap our App
component with Provider
and pass as props the store
which has as value our current store.
- In our
index.js
file.
import React from "react"
import ReactDOM from "react-dom"
import { createStore } from "redux"
import { Provider } from "react-redux"
import "./index.css"
import App from "./App"
import reducer from "./store/reducer"
const store = createStore(reducer)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
)
Then, we need to connect our component to the redux store.
- In
Articles.js
import React from "react"
import { connect } from "react-redux"
import Article from "../components/Article/Article"
import AddArticle from "../components/AddArticle/AddArticle"
const Articles = ({ articles }) => {
const saveArticle = e => {
e.preventDefault()
// the logic will be updated later
}
return (
<div>
<AddArticle saveArticle={saveArticle} />
{articles.map(article => (
<Article key={article.id} article={article} />
))}
</div>
)
}
const mapStateToProps = state => {
return {
articles: state.articles,
}
}
export default connect(mapStateToProps)(Articles)
Here, we first import connect()
, a function that returns a higher-order function and receive as input a component. It helps us to connect our component to the store and give access to get the state.
Then, we declare a new function named mapStateToProps()
(you can name it whatever you like). It's used to get our state from the redux store. The function receives as parameter the state
stored in redux and returns a JavaScript object that will hold our articles
.
And to reach the store, we need to pass mapStateToProps()
to the connect
function. It will take our component Articles
and return a wrapper component with the props it injects. That means we can now get our state from the store. The state is received by the component through props, we can still show the articles
as before but now through redux.
We've successfully connected our store to react and get our state from it. Now, let's dive into actions
6. What is action?
An action is a payload of information that contains a type like REMOVE_ARTICLE
or ADD_ARTICLE
etc. Actions are dispatched from your component. It sends data from your react component to your redux store. The action does not reach the store, it's just the messenger. The store is changed by reducer.
To create an action in our project, we need to create in our store
folder a new file named actionTypes.js
.
export const ADD_ARTICLE = "ADD_ARTICLE"
Then, we need to go to our Articles.js
file and add the following code.
import React from "react"
import { connect } from "react-redux"
import Article from "../components/Article/Article"
import AddArticle from "../components/AddArticle/AddArticle"
import * as actionTypes from "../store/actionTypes"
const Articles = ({ articles, saveArticle }) => (
<div>
<AddArticle saveArticle={saveArticle} />
{articles.map(article => (
<Article key={article.id} article={article} />
))}
</div>
)
const mapStateToProps = state => {
return {
articles: state.articles,
}
}
const mapDispatchToProps = dispatch => {
return {
saveArticle: article =>
dispatch({ type: actionTypes.ADD_ARTICLE, articleData: { article } }),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Articles)
Then, we need to import everything from actionTypes.js
. And create a new function mapDispatchToProps
which receive a function dispatch
as parameter. The mapDispatchToProps
return an object which has a property saveArticle
. It's a reference to a function that will dispatch an action in our store.
saveArticle
holds an anonymous function that receives our article
as an argument and returns the dispatch
function. It receives the type and the data to update as parameters. And as you guess, it will dispatch the action in our store.
Finally, we need to pass mapDispatchToProps
as second argument to the connect
function. And to make it work, we need to update our reducer and add the action ADD_ARTICLE
.
- In
store/reducer.js
import * as actionTypes from "./actionTypes"
const initialState = {
articles: [
{ id: 1, title: "post 1", body: "Quisque cursus, metus vitae pharetra" },
{ id: 2, title: "post 2", body: "Quisque cursus, metus vitae pharetra" },
],
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.ADD_ARTICLE:
const newArticle = {
id: Math.random(), // not really unique but it's just an example
title: action.article.title,
body: action.article.body,
}
return {
...state,
articles: state.articles.concat(newArticle),
}
}
return state
}
export default reducer
As you can see, we import our actionTypes
. Then, we check in our reducer
function if the action's type is equal to ADD_ARTICLE
. If it's the case, first create a new object which holds our article then append it to our articles array. Before we return the state, we copy the old state, then concat
it with the new article. In that way, we keep our state safe and immutable.
7. How to handle asynchronous code with redux?
The reducer as I say earlier handles only synchronous code. To execute the asynchronous code, we need to use an action creator. It's a function that returns a function or an action I should say. So, to use it in our project, we need to create a new file actionCreators.js
.
- In
store/actionCreators.js
import * as actionTypes from "./actionTypes"
export const addArticle = article => {
return {
type: actionTypes.ADD_ARTICLE,
article,
}
}
Here, we declare a new action creator named addArticle
. It's a function that receives the article
as an argument and returns the type of the action and the value. By the way, article
is the same as article: article
, it's just an ES6 convenient syntax. Now we can move on and change the function mapDispatchToProps
in the Articles.js
file.
- In
Articles.js
import React from "react"
import { connect } from "react-redux"
import Article from "../components/Article/Article"
import AddArticle from "../components/AddArticle/AddArticle"
import { addArticle } from "../store/actionCreators"
const Articles = ({ articles, saveArticle }) => (
<div>
<AddArticle saveArticle={saveArticle} />
{articles.map(article => (
<Article key={article.id} article={article} />
))}
</div>
)
const mapStateToProps = state => {
return {
articles: state.articles,
}
}
const mapDispatchToProps = dispatch => {
return {
saveArticle: article => dispatch(addArticle(article)),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Articles)
As you can see, we first import our action creator addArticle
, then in the mapDispatchToProps
function, we update the argument passed to dispatch
. Now, it receives the action creator and its value article
.
But we've not done yet, we need to add a new package redux-thunk
to our project to be able to handle asynchronous code.
npm install redux-thunk
redux-thunk
is a middleware that will help us handle the asynchronous code. Middleware provides a way to interact with actions that have been dispatched to the store before they reach the reducer. Now let's implement it to our project.
- In
index.js
import React from "react"
import ReactDOM from "react-dom"
import { createStore, applyMiddleware } from "redux"
import { Provider } from "react-redux"
import thunk from "redux-thunk"
import "./index.css"
import App from "./App"
import reducer from "./store/reducer"
const store = createStore(reducer, applyMiddleware(thunk))
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
)
In this code block, we first import applyMiddleware
from redux and thunk
from redux-thunk. Then to make it work, we need to pass to createStore
a second argument or enhancer which receives our middleware thunk
. By doing this, we are now able to deal with asynchronous code. Let's now update our action creator.
- In
store/actionCreators.js
import * as actionTypes from "./actionTypes"
export const addArticle = article => {
return {
type: actionTypes.ADD_ARTICLE,
article,
}
}
export const simulateHttpRequest = article => {
return dispatch => {
setTimeout(() => {
dispatch(addArticle(article))
}, 3000)
}
}
For this post, we will just simulate an HTTP request.
Here, we have a new action creator simulateHttpRequest
which receives the article
as input and return a function. Due to the thunk
middleware, we can access to dispatch
because our middleware runs between the dispatching of our action and the point of time the action reaches the reducer. Therefore, we can get dispatch
as an argument. Then, wait 3 seconds with setTimeout
to just simulate an HTTP request before dispatching the action and add the article to our array of articles.
We've changed our action creators a little bit, to make it work again, we need to update Articles.js
.
- In
Articles.js
import React from "react"
import { connect } from "react-redux"
import Article from "../components/Article/Article"
import AddArticle from "../components/AddArticle/AddArticle"
import { simulateHttpRequest } from "../store/actionCreators"
const Articles = ({ articles, saveArticle }) => (
<div>
<AddArticle saveArticle={saveArticle} />
{articles.map(article => (
<Article key={article.id} article={article} />
))}
</div>
)
const mapStateToProps = state => {
return {
articles: state.articles,
}
}
const mapDispatchToProps = dispatch => {
return {
saveArticle: article => dispatch(simulateHttpRequest(article)),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Articles)
Here, the only thing we have to do is change addArticle
to simulateHttpRequest
, in that way, everything should work again, and now we're able to handle asynchronous code through redux.
You can find the finished project here
Conclusion
When it comes to dealing with medium size to bigger react apps, managing our state can be really hard. And a package like redux can make it very easy. There are also some alternatives like the context API (+hooks) which is very helpful and does not require a third-party library, but dive into redux is still relevant.
However, redux is overkill for simple React app like our project, we don't need redux to manage our state, but it's easier to understand how redux works with a very simple app.
Resources
React Redux official documentation
Redux devtools
Redux Best practices
Redux saga
The Context API