Engineer's Tutorial
Aug 18, 202412 min read
Complete Beginner's Guide to Redux: Learn State Management from Scratch
RY

Rohan Yadav

Full stack developer

Table of contents

 

A Beginner’s Guide to Redux: Step by Step Explanation with Examples

 

 

Introduction

Hello there! 🌟 If you're new to web development, you might have heard about React and Redux. Maybe you're wondering, "What exactly are these, and why do I need them?" Don't worry—I'm here to guide you through everything from the basics.

Let’s start from scratch and understand why Redux is important and how to use it. We'll also cover how to set up a simple Redux application, even if you haven't worked with React before. Ready? Let’s dive in!

 

What is Redux?

Imagine you're building a big app, like a social media platform. As your app grows, you need a way to keep track of all the information (like user data, posts, comments) in one organized place. That's where **Redux** comes in.

Redux is like a giant container where you store all the information (or "state") of your app. This makes it easier to manage, especially as your app becomes more complex. Redux helps you keep everything in sync, so different parts of your app can share and update information without getting messy.

 

Why Do We Need Redux?

In a small app, you might not need Redux. But as your app grows, you might find it hard to keep track of what's going on. For example:

  • How do you make sure all parts of your app know when a user logs in?
  • How do you update the app when new data arrives without causing bugs?

Redux solves these problems by providing a structured way to manage your app’s data (or "state").

 

The Three Key Concepts of Redux

1. Actions: Think of actions as “messages” that you send to Redux to tell it that something happened. For example, “A user just logged in” or “A new post was added.”
  
2. Reducers: Reducers are functions that tell Redux how to update the app's state based on the action it received. For example, when a user logs in, the reducer will update the state to store that user's information.

3. Store: The store is where all the app's state lives. It’s the big box that holds everything together.


 

Let’s Build a Simple Example

Imagine you're building a simple to-do list app where users can add tasks. We’ll use Redux to manage this app’s state.

If you have already installed react and necessary library, skip step 1

Step 1: Set Up Your Project

First, let's create a new project. You can use a tool called Create React App to set up everything you need:

npx create-react-app my-redux-app

Copy the above command and paste into you vs-code terminal or command prompt and hit [Enter]. Press Y and enter to begin he installation

Once the installation is finished, you will see the success message in terminal

 

Navigate to you project >> cd my-redux-app

Paste below command to install the necessary package

npm install redux react-redux

This command will create a new React project and install Redux and React-Redux, which helps React work with Redux.

Your folder structure will should look like this

 
Step 2: Create Actions

Let’s start by creating some actions. Remember, actions are like messages you send to Redux. For our to-do app, we might need actions like “Add a new task” and “Delete a task.”

Create a new folder called actions inside the src directory and add a file named index.js:

// src/actions/index.js
export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const addTodo = (todo) => ({
 type: ADD_TODO,
 payload: todo
});
export const deleteTodo = (index) => ({
 type: DELETE_TODO,
 payload: index
});

Explanation

  • ADD_TODO and DELETE_TODO are action types. They describe the kind of action we want to perform.
  • addTodo and deleteTodo are action creators. These are functions that return the actual action objects.
 
Step 3: Create Reducers

Next, we need to create a reducer. A reducer will decide how our state should change when an action is dispatched (or sent) to Redux.

Create a new folder called reducers inside src and add a file named todoReducer.js:

// src/reducers/todoReducer.js
import { ADD_TODO, DELETE_TODO } from '../actions';
const initialState = {
 todos: []
};
const todoReducer = (state = initialState, action) => {
 switch (action.type) {
   case ADD_TODO:
     return {
       ...state,
       todos: [...state.todos, action.payload]
     };
   case DELETE_TODO:
     const newTodos = state.todos.filter((_, index) => index !== action.payload);
     return {
       ...state,
       todos: newTodos
     };
   default:
     return state;
 }
};
export default todoReducer;

Explanation

  • initialState: This is the starting point of our app’s state. We begin with an empty list of todos.
  • The todoReducer function checks the type of action (ADD_TODO or DELETE_TODO) and decides how to update the state.
 
Step 4: Combine Reducers (If Needed)

If you have more than one reducer (e.g., one for todos, one for user login), you need to combine them. For now, since we only have one reducer, we’ll just create a root reducer.

Create a file named index.js inside the reducers folder:

// src/reducers/index.js
import { combineReducers } from 'redux';
import todoReducer from './todoReducer';
const rootReducer = combineReducers({
 todos: todoReducer
});
export default rootReducer;

Explanation
combineReducers is a function that lets us combine multiple reducers into one. Even though we only have one reducer now, it's a good habit to use combineReducers for scalability.

 

Step 5: Create the Store

Now we need to create the Redux store, where all the state lives. The store will use our reducer to manage the state.

Create a new folder called store inside src and add a file named index.js:

// src/store/index.js
import { createStore } from 'redux';
import rootReducer from '../reducers';
const store = createStore(rootReducer);
export default store;

Explanation
createStore: This function creates the Redux store using our root reducer.

 

Step 6: Provide the Store to Your App

To make Redux available in our app, we need to wrap our main component with a Provider. This makes the Redux store available to any component in our app.

Edit the src/index.js file:

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
 <Provider store={store}>
   <App />
 </Provider>
);

Explanation
The Provider component wraps our entire app and gives it access to the Redux store.

 

Step 7: Connect Your Components to Redux

Now that everything is set up, let's connect our React components to Redux.

Example 1: Displaying the To-Do List

Create a new file called TodoList.js inside the src/components folder:

// src/components/TodoList.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { deleteTodo } from '../actions';
const TodoList = () => {
 const todos = useSelector((state) => state.todos.todos);
 const dispatch = useDispatch();
 return (
   <ul>
     {todos.map((todo, index) => (
       <li key={index}>
         {todo} <button onClick={() => dispatch(deleteTodo(index))}>Delete</button>
       </li>
     ))}
   </ul>
 );
};
export default TodoList;

Explanation
useSelector: This hook lets you access the Redux state. Here, we use it to get the list of todos.
useDispatch: This hook gives you the dispatch function, which you can use to send actions to Redux.

Example 2: Adding a New To-Do

Create a new file called AddTodo.js inside the src/components folder:

// src/components/AddTodo.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from '../actions';
const AddTodo = () => {
 const [input, setInput] = useState('');
 const dispatch = useDispatch();
 const handleSubmit = (e) => {
   e.preventDefault();
   if (input.trim()) {
     dispatch(addTodo(input));
     setInput('');
   }
 };
 return (
   <form onSubmit={handleSubmit}>
     <input
       type="text"
       value={input}
       onChange={(e) => setInput(e.target.value)}
     />
     <button type="submit">Add Todo</button>
   </form>
 );
};
export default AddTodo;

Explanation
useState: This React hook is used to manage the input state.
handleSubmit: This function adds a new to-do when the form is submitted.

 

Step 8: Putting It All Together

Finally, let's put it all together in the `App.js` file:

// src/App.js
import React from 'react';
import TodoList from './components/TodoList';
import AddTodo from './components/AddTodo';
const App = () => (
 
<div>
   <h1>My To-Do List</h1>
   <AddTodo />
   <TodoList />
 </div>
);

Explanation
This is our main component that displays the header, the input form for adding a new to-do, and the list of existing to-dos.

 

Let's outline the folder structure for the Redux-based application we've discussed. This structure will help you keep your files organized as your project grows.

Folder Structure Overview

my-redux-app/
├── node_modules/
├── public/
│   ├── index.html
│   └── favicon.ico
├── src/
│   ├── actions/
│   │   └── index.js
│   ├── components/
│   │   ├── AddTodo.js
│   │   └── TodoList.js
│   ├── reducers/
│   │   ├── index.js
│   │   └── todoReducer.js
│   ├── store/
│   │   └── index.js
│   ├── App.js
│   ├── index.js
│   └── index.css
├── .gitignore
├── package.json
└── README.md

 

Detailed Explanation of Each Folder and File
  1. node_modules/:
    • This folder contains all the dependencies (packages) installed via npm. You typically don't need to touch this folder directly.
  2. public/:
    • index.html: The main HTML file. It serves as the entry point for your React application. The root div here is where your entire React app gets rendered.
    • favicon.ico: The icon that appears in the browser tab for your site.
  3. src/: This is the main directory where all your source code lives.
    • actions/:
      • index.js: This file contains all the action types and action creators. Actions describe events that occur in the application and are dispatched to update the state.
    • components/:
      • AddTodo.js: A React component that handles adding new to-dos. It includes a form and the logic to dispatch the action to add a new to-do.
      • TodoList.js: A React component that displays the list of to-dos. It uses useSelector to access the to-dos from the Redux store and useDispatch to delete a to-do.
    • reducers/:
      • index.js: The root reducer, where you combine multiple reducers (if you have more than one) into a single reducer.
      • todoReducer.js: This reducer handles the state for the to-do list, including adding and deleting to-dos and managing the state during API fetch operations.
    • store/:
      • index.js: This file sets up the Redux store using the root reducer and applies any middleware (like redux-thunk).
    • App.js:
      • The main React component where the application is composed. It includes the main structure of the app, bringing together different components like TodoList and AddTodo.
    • index.js:
      • The entry point for the React application. It renders the App component into the index.html file's root div. This file also includes the Provider component from react-redux to connect your React app with Redux.
    • index.css:
      • The global CSS file where you can include styles for your entire application.
  4. .gitignore:
    • A file that tells Git which files and directories to ignore in version control. Typically, you’ll ignore things like node_modules/, build files, and environment variables.
  5. package.json:
    • This file contains metadata about your project, including dependencies, scripts, and other configurations.
  6. README.md:
    • A markdown file that serves as the main documentation for your project. You can describe your app, how to run it, and any other important information here.

 

Adding Middleware to Your Redux Application

Middleware in Redux is like a translator that sits between the action and the reducer. It can intercept actions before they reach the reducer, allowing you to modify them, log them, or even stop them entirely. Middleware is essential for handling asynchronous actions, like fetching data from an API.

Let’s dive into how middleware works in Redux and how you can use it to enhance your app.

What is Middleware?

Middleware is a piece of code that runs between dispatching an action and the moment it reaches the reducer. It’s like an extra layer of functionality that you can add to your Redux flow.

Common use cases for middleware include:

  • Logging: Track every action dispatched and the resulting state.
  • Asynchronous Actions: Handle things like API calls where you need to dispatch actions after receiving data.
  • Conditional Dispatch: Stop or modify actions based on certain conditions.

 

How to Add Middleware in Redux

Redux comes with a few built-in middlewares like redux-thunk and redux-saga. Let’s focus on redux-thunk, as it’s simpler and commonly used.

Step 1: Install Redux Thunk

First, install the redux-thunk package: npm install redux-thunk

 

Step 2: Apply Middleware to Your Store

To use middleware, you need to apply it when you create the Redux store. Let’s modify our store/index.js file to include redux-thunk:

// src/store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);
export default store;

Explanation:

  • applyMiddleware(thunk): This function adds the thunk middleware to the Redux store, enabling you to handle asynchronous actions.

 

Using Middleware for Asynchronous Actions

Now that we have middleware set up, let's see it in action by fetching data from an API.

Step 3: Create Asynchronous Action Creators

Let’s modify our to-do app to fetch initial tasks from an API when the app loads.

First, we’ll create a new action type and an asynchronous action creator to fetch the data:

// src/actions/index.js
export const FETCH_TODOS_REQUEST = 'FETCH_TODOS_REQUEST';
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS';
export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE';
export const fetchTodos = () => {
  return async (dispatch) => {
    dispatch({ type: FETCH_TODOS_REQUEST });
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/todos');
      const data = await response.json();
      dispatch({ type: FETCH_TODOS_SUCCESS, payload: data });
    } catch (error) {
      dispatch({ type: FETCH_TODOS_FAILURE, payload: error.message });
    }
  };
};

Explanation:

  • FETCH_TODOS_REQUEST, FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE: These action types represent the different states of an API call (starting, success, failure).
  • fetchTodos: This action creator uses redux-thunk to perform an asynchronous operation. It first dispatches a request action, then attempts to fetch data from the API, and finally dispatches either a success or failure action depending on the result.

 

Step 4: Update the Reducer to Handle New Actions

Next, we need to update our reducer to handle these new actions:

// src/reducers/todoReducer.js
import { ADD_TODO, DELETE_TODO, FETCH_TODOS_REQUEST, FETCH_TODOS_SUCCESS, FETCH_TODOS_FAILURE } from '../actions';
const initialState = {
  todos: [],
  loading: false,
  error: null
};
const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
    case DELETE_TODO:
      return {
        ...state,
        todos: state.todos.filter((_, index) => index !== action.payload)
      };
    case FETCH_TODOS_REQUEST:
      return {
        ...state,
        loading: true,
        error: null
      };
    case FETCH_TODOS_SUCCESS:
      return {
        ...state,
        loading: false,
        todos: action.payload
      };
    case FETCH_TODOS_FAILURE:
      return {
        ...state,
        loading: false,
        error: action.payload
      };
    default:
      return state;
  }
};
export default todoReducer;

Explanation:

  • The reducer now handles additional actions related to the asynchronous fetch operation, updating the state to show loading, success, or error.

 

Step 5: Dispatch Asynchronous Action in a Component

Finally, let’s trigger the fetchTodos action when our app loads:

// src/App.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTodos } from './actions';
import TodoList from './components/TodoList';
import AddTodo from './components/AddTodo';
const App = () => {
  const dispatch = useDispatch();
  const loading = useSelector((state) => state.todos.loading);
  const error = useSelector((state) => state.todos.error);
  useEffect(() => {
    dispatch(fetchTodos());
  }, [dispatch]);
  return (
    <div>
      <h1>My To-Do List</h1>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      <AddTodo />
      <TodoList />
    </div>
  );
};
export default App;

Explanation:

  • useEffect: This React hook is used to trigger the fetchTodos action as soon as the component mounts.
  • The app now displays a loading message while the data is being fetched and shows an error message if something goes wrong.

 

Conclusion

Congratulations! 🎉 You've just built a simple to-do app using Redux. Even if this is your first time, you've managed to:

  • Understand the core concepts of Redux (actions, reducers, store).
  • Set up a Redux store and connect it to a React app.
  • Use Redux to manage the state of a simple to-do list.

As you continue to explore, you'll discover more advanced features and use cases. But for now, you've taken a big step towards mastering Redux!