Managing state in a React application can become complex as the application grows and the number of components increases. One way to manage state in a React application is by using Redux, a state management library. In this tutorial, we will walk through the basics of setting up and using Redux in a simple blogging app.
About Redux
Redux is a popular library for managing state in a React application. It allows you to centralize your application’s state and actions in a single store, making it easier to manage, debug, and test. In this article, we’ll go over the basics of setting up Redux in a React application and demonstrate how it can be used to manage state. Redux consists of three main concepts:
- store, which holds the global state of the app and has methods for interacting with it;
- actions: In Redux, actions plain JavaScript objects that describe events and are dispatched to the store; and,
- reducers, which calculate new states based on provided action objects and maintain immutability in state updates. The
dispatch
method is the only way to update state in a Redux app.
Data Flow: Redux vs React App
In a regular React app, the application state is defined within individual component files and the UI is rendered based on that state. When an event occurs, such as a user entering a form or clicking a button, the state is updated and the UI is re-rendered to reflect the updated state.
In contrast, in a Redux app, the state is stored in a central location, the Redux store. When an event occurs, an action is dispatched to the store, and a reducer function is run to update the state. The connected components are then notified of the state change and re-render to display the updated state if necessary.
Setting Up Redux
To get started with Redux, you’ll first need to install it in your project. You can do this using npm:
$ npm install --save redux
Next, you’ll need to create a Redux store. A store is an object that holds the application’s state and allows you to dispatch actions to modify the state. To create a store, you’ll need to provide it with a reducer function. A reducer is a pure function that takes in the current state and an action, and returns a new state.
Here’s an example of a reducer function:
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 }
case "DECREMENT":
return { count: state.count - 1 }
default:
return state
}
}
The reducer function above handles two types of actions: INCREMENT
and DECREMENT
. When the INCREMENT
action is dispatched, the reducer function returns a new state with the count
property incremented by 1. When the DECREMENT
action is dispatched, the reducer function returns a new state with the count
property decremented by 1.
To create a store, you can use the createStore function from the redux library and pass in your reducer function as an argument:
import { createStore } from "redux"
const store = createStore(reducer)
Now that you have a store, you can use it to manage the state of your application.
Dispatching Actions
To modify the state of your application, you’ll need to dispatch actions. An action is an object that describes a change that you want to make to the state. In the example above, the INCREMENT
and DECREMENT
actions are dispatched to modify the count property of the state.
To dispatch an action, you can use the dispatch method of the store:
store.dispatch({ type: "INCREMENT" })
This will cause the reducer function to be called with the current state and the INCREMENT
action. The reducer function will then return a new state with the count
property incremented by 1.
Subscribing to Changes
Whenever an action
is dispatched, the reducer function is called and the state is updated. If you want to be notified when the state changes, you can subscribe to the store using the subscribe
method:
store.subscribe(() => {
console.log("State updated:", store.getState())
})
This will log the current state to the console every time an action is dispatched and the state is updated.
Connecting Redux to React
Now that you have a Redux store set up, you can connect it to your React application. To do this, you’ll need a couple of packages:
react-redux
library, which provides a set of bindings between Redux and Reactredux-thunk
is a middleware for Redux that allows you to write action creators that return a function instead of an action object, enabling asynchronous actions such as making API calls and dispatching multiple actions.
In Redux, middleware is a way to extend the functionality of the Redux store. It sits between the store and the action creators, and allows you to intercept actions as they are dispatched and modify or add additional functionality to them before they reach the store. This can be useful for tasks such as logging, performing asynchronous actions, or dispatching multiple actions in response to a single action.
$ npm install react-redux redux-thunk
Getting Started
Create a store
directory inside of the src
directory. Next, define the action types
and action creators
:
src/store/actions.js
// action types
export const FETCH_POSTS = "FETCH_POSTS"
export const FETCH_POST = "FETCH_POST"
export const CREATE_POST = "CREATE_POST"
export const UPDATE_POST = "UPDATE_POST"
export const DELETE_POST = "DELETE_POST"
// action creators
export const fetchPosts = () => ({
type: FETCH_POSTS,
})
export const fetchPost = id => ({
type: FETCH_POST,
payload: id,
})
export const createPost = post => ({
type: CREATE_POST,
payload: post,
})
export const updatePost = post => ({
type: UPDATE_POST,
payload: post,
})
export const deletePost = id => ({
type: DELETE_POST,
payload: id,
})
The action types
are usually defined as string constants. This helps to ensure that the type
field of an action is always a string and that the type field is spelled correctly throughhout the entire app.
The action creators
are functions that return action objects with a type field and a payload field. The type
field is a string that identifies the action, and the payload
field is the data that is associated with the action.
Next, create the posts reducer fuction:
src/store/postsReducer
const initialState = {
posts: [],
post: {},
}
const postsReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_POSTS:
return {
...state,
posts: action.payload,
}
case FETCH_POST:
return {
...state,
post: action.payload,
}
case CREATE_POST:
return {
...state,
posts: [...state.posts, action.payload],
}
case UPDATE_POST:
return {
...state,
posts: state.posts.map(post =>
post.id === action.payload.id ? action.payload : post
),
}
case DELETE_POST:
return {
...state,
posts: state.posts.filter(post => post.id !== action.payload),
}
default:
return state
}
}
export default postsReducer
The reducer
has an initial state with two fields: posts
, which is an array of all the posts, and post
, which is an object representing a single post. When an action
is dispatched, the reducer
handles each by updating the state
accordingly.
The api functions can be defined in a seperate file:
src/store/services.js
import axios from "axios"
import { CREATE_POST, FETCH_POSTS, FETCH_POST } from "./actions"
const api = axios.create({
baseURL: "http://localhost:8000/api", // switch with your server address
})
// create a post
export const createPost = post => async dispatch => {
const response = await api.post("/posts", post)
dispatch({ type: CREATE_POST, payload: response.data })
}
// fetch all posts
export const fetchPosts = () => async dispatch => {
const response = await api.get(`/posts`)
dispatch({ type: FETCH_POSTS, payload: response.data })
}
// fetch one post
export const fetchPost = id => async dispatch => {
const response = await api.get(`/posts/${id}`)
dispatch({ type: FETCH_POST, payload: response.data })
}
-
These functions use the
redux-thunk
middleware to handle the async logic of making the API calls. Themiddleware
allows these functions to be dispatched like any other action, but with the ability to perform async operations before dispatching the actual action. -
The
createPost
function makes an API call to create a new post on the backend, and then dispatches a synchronous action with the typeCREATE_POST
and the new post data as the payload. TheCREATE_POST
action is then handled by thereducer
, which updates the state with the new post. -
The
fetchPosts
function makes an API call to retrieve a single post from the backend, and then dispatches a synchronous action with the typeFETCH_POST
and the post data as the payload. These types are imported from the previously declaredactions.js
file where we declared them as constants.
To complete the redux store setup, we initialize the store itself. You will need to use the createStore
function, provided by the redux library. This function takes a reducer function as an argument and returns a new store object.
The combineReducers
function, also from the redux library, is used to combine multiple reducer functions into a single reducer function that you can pass to createStore
.
src/store/index.js
import { createStore, combineReducers, applyMiddleware, compose } from "redux"
import postsReducer from "./posts/postsReucer"
import { composeWithDevTools } from "redux-devtools-extension"
import thunk from "redux-thunk"
const rootReducer = combineReducers({
posts: postReducer,
// other reducers
})
const store = createStore(
rootReducer,
compose(applyMiddleware(thunk), composeWithDevTools())
)
export default store
The compose
function is a utility function provided by the redux
library that allows you to apply a series of functions to a value, starting from the rightmost function and working towards the left. In the above setup, composeWithDevTools
function is, therefore, applied first, followed by the applyMiddleware
function. By applying the functions from right to left, you can ensure that the functions are applied in the correct order. In the case of applying middleware and the DevTools to a store, this means that the DevTools are applied first, followed by the middleware.
For example, if you want to apply the thunk middleware and the DevTools to a store, you would want the DevTools to be applied first, so that you can use the DevTools to debug the actions dispatched by the thunk middleware. If you applied the functions in the opposite order, the DevTools would be applied after the thunk middleware, which would not allow you to debug the actions dispatched by the thunk middleware.
applyMiddleware is a function provided by the redux
library that allows you to apply middleware to a Redux store. Usually, you pass one or more middlewares such as a logger middleware or other custom middleware:
applyMiddleware(thunk, logger)
In this setup, the applyMiddleware
function is used to apply the thunk
middleware to the store. The composeWithDevTools
function is then applied to add the DevTools. The resulting store will have the Redux DevTools and the thunk middleware enabled.
Redux DevTools
This is a powerful tool to help with checking and interacting with your redux store from the browser. To use it, you need to have the Chrome/Firefox extension, Redux Devtools
Next is to configure your project to use the devtools, there are two ways to achieve this:
1. using redux-devtools extension:
$ npm install --save-dev redux-devtools
Next, import the composeWithDevTools
function from the redux-devtools-extension library and pass it as an argument to the createStore
function:
const store = createStore(rootReducer, composeWithDevTools())
// OR
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
// OR
const store = createStore(
rootReducer,
compose(applyMiddleware(thunk), composeWithDevTools())
)
2. Using Global Window Object
Another way is to use the global window.__REDUX_DEVTOOLS_EXTENSION__
function, which is provided by the Redux DevTools browser extension when it is installed. To use this method, you can pass the window.__REDUX_DEVTOOLS_EXTENSION__
function as an argument to createStore
:
//...
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
Next, you’ll need to wrap your root React component with the Provider
component from react-redux
. The Provider
component allows you to pass the store
to your React components, making it available to them through the useSelector
and useDispatch
hooks:
import { Provider } from "react-redux"
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
)
With the Provider
component in place, you can use the useSelector
and useDisptach
hooks in from any of your React components to access the state from the store. To do this, add two components components: a postsList
component that displays a list of blog posts, and an addPostForm
component that allows users to add new posts to the list.
src/AddPostForm.js
import React, { useState } from "react"
import { useDispatch } from "react-redux"
import { createPost } from "./actions"
const AddPostForm = () => {
const [title, setTitle] = useState("")
const [body, setBody] = useState("")
const dispatch = useDispatch()
const handleSubmit = e => {
e.preventDefault()
dispatch(createPost({ title, body }))
setTitle("")
setBody("")
}
return (
<form onSubmit={handleSubmit}>
<label>
Title:
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</label>
<br />
<label>
Body:
<textarea value={body} onChange={e => setBody(e.target.value)} />
</label>
<br />
<button type="submit">Add Post</button>
</form>
)
}
export default AddPostForm
This component has a form with two input fields for the title
and body
of the post, and a submit
button. When the form is submitted, the component uses the useDispatch
hook to access the dispatch
function from the Redux store
. The dispatch
function is used to dispatch the createPost
action with the title and body of the new post.
src/PostsList.js
import React from "react"
import { useSelector } from "react-redux"
const PostsList = () => {
const posts = useSelector(state => state.posts)
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
))}
</div>
)
}
export default PostsList
This component uses the useSelector
hook to access the posts
state from the Redux store. The useSelector
hook takes a selector function as an argument, which receives the entire state and returns the specific piece of state that the component needs. In this case, the selector function simply returns the posts
state.
The postsList
component then maps over the posts array and renders a list of posts using the map function.
Conclusion
In this article, we covered the basics of setting up and using Redux in a React application. We looked at how to create a Redux store
, dispatch actions, and subscribe to changes in the state. We also learned how to connect Redux to React using the react-redux
library and making asynchronous calls to the backend using redux-thunk
middleware. Next, see how to improve the redux workflow with Redux Toolkit
here.