Beginner's Guide To Redux

Published on June 25, 2022

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:

  1. store, which holds the global state of the app and has methods for interacting with it;
  2. actions: In Redux, actions plain JavaScript objects that describe events and are dispatched to the store; and,
  3. 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 React
  • redux-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. The middleware 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 type CREATE_POST and the new post data as the payload. The CREATE_POST action is then handled by the reducer, 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 type FETCH_POST and the post data as the payload. These types are imported from the previously declared actions.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.