A step-by-step Guide to using Redux for State Management in React

A step-by-step Guide to using Redux for State Management in React

Redux is a state management tool for JavaScript applications.
It provides a store for our applications and methods to get the state, listen to changes in your state, and update it.
In this article, you will learn how to use Redux in your React application by creating a simple todo application.

Repository

Check out the complete source code here

Prerequisites

The todo Application will be built with React and Redux.

  • Basic Javascript Knowledge

  • Node installed on your device

  • Basic understanding of React

Three Main Parts of Redux

Redux contains three main parts:

  • Store: a state container that holds all the application state and provides methods to interact with the state.

  • Actions: a plain object describing the change that occurred in your application. It must contain a type field indicating the action being performed.

  • Reducers: reducer is a function that takes the current state and the action which occurred and returns a new state.

Setting up Redux in a React application

Step 1: Install Redux and react-redux

    npm install redux react-redux

Step 2: Create a Redux folder in your src directory

The src/redux directory will contain your reducers, actions, and store.

Step 3: Create a todo folder in your redux folder

The src/redux/todo will contain your todoActions.js and todoReducer.js files.

Step 4: Create your actions

For each action object, add the type of the action and the payload, if necessary.

src/redux/todo/todoActions.js

export const addTask = (data) => ({
      type: "ADD_TASK",
      payload: data,
    });

    export const fetchTasks = (data) => ({
      type: "FETCH_TASK",
      payload: data,
    });

    export const deleteTasks = (task) => ({
      type: "DELETE_TASK",
      payload: task,
    });

    export const updateTasks = (task) => ({
      type: "UPDATE_TASK",
      payload: task,
    });

step 5: Create your reducers

Define the initial state of your todo application and create a Reducer function that takes in your initial state and action as parameters. State in react is immutable, meaning it should not change directly. So for every condition, a copy of the state is first made using the spread operator ...state.

src/redux/todo/todoReducers.js

const INITIAL_STATE = {
      todos: [],
    };

    const todoReducer = (state = INITIAL_STATE, action) => {
      switch (action.type) {
        case "ADD_TASK":
          return {
            ...state,
            todos: [...state.todos, action.payload],
          };

        case "FETCH_TASK":
          return {
            ...state,
            todos: action.payload,
          };

        case "UPDATE_TASK":
          return {
            ...state,
            todos: state.todos.map((todo) => {
              if (todo.id === action.payload) {
                todo.title = action.payload.title;
              }
            }),
          };

        case "DELETE_TASK":
          return {
            ...state,
            todos: state.todos.filter((todo) => todo.id !== action.payload),
          };
        default:
          return state;
      }
    };

    export default todoReducer;

step 6: Create your root-reducer

As the application grows more complex, it can contain multiple reducers which manage independent parts of your state; with the help of the combineReducers helper function provided by the Redux library, you can import all the reducers in the application, and export a single root reducer file.

src/redux/root-reducer.js

  import { combineReducers } from "redux";
    import todoReducer from "./todo/todoReducer";

    const rootReducer = combineReducers({
      todo: todoReducer,
    });

    export default rootReducer;

Step 7: Create the store

Create a store.js file in the src/redux folder. Add a middleware called redux-logger to help log your state changes in the console.

src/redux/store.js

 import { createStore, applyMiddleware } from "redux";
    import logger from "redux-logger";
    import rootReducer from "./root-reducer";

    const middlewares = [];

    if (process.env.NODE_ENV === "development") {
      middlewares.push(logger);
    }

    export const store = createStore(rootReducer, applyMiddleware(...middlewares));

    export default store;

step 8: Connect your Redux store to your application

Connecting your Redux store to your application requires wrapping your main app component in your src/index.js file with a Provider component, and passing your store into the provider component like a prop.

src/index.js

   import { Provider } from "react-redux";
    import store from "./Redux/store";
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(
      <Provider store={store}>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </Provider>
    );

Create a demo rest JSON web service using json-server and a db.json file with sample data in the src folder.
Redux has libraries like Redux-thunk for performing asynchronous tasks and data fetching, but handling asynchronous tasks in this application will be done in components.

Install json webserver

   npm install -g json-sever

Using Redux from the components

Now that you have set up the redux store, you can consume state and dispatch actions using useSelector and useDispatch hooks.

This form is the input form to create a task

AddForm.jsx

 <form onSubmit={handleSubmit}>
        <input
          name="task"
          placeholder="Add a task"
          type="task"
          value={task}
          onChange={handleChange}
        />
        <button>Add task</button>
      </form>

And in the handleSubmit handler function, make a request to the demo web service to add a task and dispatch our addTask action.

handleSubmit

 const handleSubmit = async (e) => {
    e.preventDefault();

    const {data} = await axios.post("http://localhost:4000/tasks", {
      title: task,
    });
    dispatch(addTask(data));

  };

Fetch All Tasks

Fetch all tasks in the app.js component and pass them to the AddForm.jsx component as a prop for it to be rendered.
In the useEffect hook, send a get request to your endpoint to get all the tasks and dispatch a fetchTasks action with the data obtained from the response as the payload.

useEffect(() => {
    async function fetchData() {
      const { data } = await axios.get("http://localhost:4000/tasks");
      dispatch(fetchTasks(data));
    }
    fetchData();
  }, []);

Deleting a Task

To delete a task, create a handleDelete function and in it, send a delete request to your endpoint.

delete task

  const handleDelete = async (id) => {
    const res = await axios.delete(`http://localhost:4000/tasks/${id}`);
    dispatch(deleteTasks(id));
  };

Updating a Task

To update a task, create an input form in which you add your new update and also a handleUpdate function that takes care of sending a patch request to the endpoint.

UpdateForm.jsx

      <form onSubmit={handleSubmit}>
        <input
          name="task"
          placeholder="Add a task"
          type="task"
          value={task}
          onChange={handleChange}
        />
        <button onClick={()=>handleUpdate(params.id)}>Update task</button>
      </form>

handleUpdate

  const handleUpdate = async (id) => {

    const response = await axios.patch(`http://localhost:4000/tasks/${id}`, {
      title: task,
    });
    dispatch(updateTasks(task))
  };

After completing the application, your demo application will look like this:

Demo of the todo app

Conclusion

When working with small-scale applications, using Redux for state management adds an extra layer of complexity that is not necessary since the state does not need to be passed down to many child components.

For large-scale applications, managing state with Redux provides a single source of truth, making it easier to understand data flow throughout the application and providing state value to different components in our application.

Resources

Redux essentials
What is Redux?