Creating a To-do list app using Redux Toolkit

Creating a To-do list app using Redux Toolkit

Introduction

Redux Toolkit is a react package for state management built on redux. In this tutorial, I’ll explain how to use the Redux toolkit by building a small to do list app that keeps its state using Redux Toolkit. I personally prefer redux toolkit over regular redux because it is much easier to use. The creators of the package intended to make it the default way to write Redux logic. They eliminated a bunch of boilerplate code and made redux state management easier to create.

The UI

I won’t be focusing on the UI in this tutorial, so I made this github repository that you can clone that has the UI already created for you. (link)

Now we have the UI that looks like this:

initial UI

Using Redux Toolkit

Now that we have our UI ready, you’ll see that I’ve hard-coded the to-do list items. Our goal in this part of the article is to make the user able to add and remove to-dos from the list.

Installing Redux Toolkit

After cloning the repository above and installing packages, you should install redux toolkit and react-redux using the following command:

npm install @reduxjs/toolkit react-redux

Creating the Todo Slice

The slice is the component that keeps track of the app’s state. In it, You should define the initial state and the functions that will be needed to change the state when necessary.

  1. To build the slice, you need to import createSlice . Which is a function inside the Redux Toolkit package.

     import { createSlice } from "@reduxjs/toolkit";
    
  2. Next, define your initial state. In this case, two variables will be part of the state:

    • count : an integer that keeps track of the number of to-dos in the app (initially equals 0)
    • todos: a list that contains all the todos that the user added (initially empty)

      const initialState = {
      count: 0,
      todos: [],
      };
      
  3. Now, use the createSlice function that was imported. It takes three arguments:

    • Name of the slice (in this case todo)
    • Initial state (defined above)
    • reducers (the functions that will be used to change the state values)

      export const todoSlice = createSlice({
      name: "todo",
      initialState,
      reducers: {
        addTodo: (state, action) => {
          const todo = {
            id: Math.random() * 100,
            text: action.payload,
          };
          state.todos.push(todo);
          state.count += 1;
        },
        removeTodo: (state, action) => {
          state.todos = state.todos.filter((todo) => todo.id !== action.payload);
          state.count -= 1;
        },
      },
      });
      

      Here, two functions were added, one is addTodo and the other is removeTodo:

    • addTodo → creates a new todo item using the text passed in the argument and assigns a random id to it. It pushes the created todo to the todos list and increases the count value.

    • removeTodo → removes the todo item using the id given in the arguments from the todos list and decrements the count value.

      Note that to get the argument passed to the function, we use action.payload. If you need more than one argument, you can send the arguments as one object and access it using the payload. For example:

      action.payload.variable
      
  4. Export the functions to be able to use them in the app. You can do so by accessing todoSlice.actions.

     export const { addTodo, removeTodo } = todoSlice.actions;
    
  5. Finally, export the reducer. This will be used in the store to be able to provide the state to the app (see next section)

     export default todoSlice.reducer;
    

Full TodoSlice code

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  count: 0,
  todos: [],
};

export const todoSlice = createSlice({
  name: "todo",
  initialState,
  reducers: {
    addTodo: (state, action) => {
      const todo = {
        id: Math.random() * 100,
        text: action.payload,
      };
      state.todos.push(todo);
      state.count += 1;
    },
    removeTodo: (state, action) => {
      state.todos = state.todos.filter((todo) => todo.id !== action.payload);
      state.count -= 1;
    },
  },
});

export const { addTodo, removeTodo } = todoSlice.actions;

export default todoSlice.reducer;

Creating the store

The store is the component that you provide to the redux provider that makes it aware of the existence of the state.

  1. Create a folder in your src folder called “store” and create an index.js file inside

    create a store folder

  2. Add the following code to the file. This will make the store listen to our reducer and that’s how we’ll be able to access the variables and the functions inside our slice. We’ll name our reducer “todo”

     import { configureStore } from "@reduxjs/toolkit";
     import todoReducer from "../features/todoSlice";
    
     const store = configureStore({
       reducer: {
         todo: todoReducer,
       },
     });
    
     export default store;
    

Adding the store to our app.

To provide the store to the app, use the normal redux provider.

In your index.js file add the following code:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
import store from "./store";
import { Provider } from "react-redux";

ReactDOM.render(
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>,
  document.getElementById("root")
);

Using the State

To use the state that was just created in the app, import useSelector and useDispatch from redux. Also, import addTodo and removeTodo from the slice file.

  1. Add the following imports to your App.js file:

     import { useSelector, useDispatch } from "react-redux";
     import { addTodo, removeTodo } from "./features/todoSlice";
    
  2. In the App function, get both the count and todo states using useSelector. Note that to access any of those states, you should access the reducer name that was specified in the store file like this:

     const count = useSelector((state) => state.todo.count);
     const todos = useSelector((state) => state.todo.todos);
    
  3. Extract the dispatch function that will be used to run addTodo and removeTodo functions.

     const dispatch = useDispatch();
    
  4. Write handleAddTodo function that will be triggered when the form is submitted.

     const handleAddTodo = (e) => {
         e.preventDefault();
         dispatch(addTodo(input));
     };
    

    Calling e.preventDefault() is necessary because the form’s default action is to refresh the page after submitting. If you let it refresh the page, all our state will be reset.

  5. For the handleTodoDone function, it will be triggered it when the user clicks on a todo item. The id of the todo will be used to identify which todo is removed.

     const handleTodoDone = (id) => {
         dispatch(removeTodo(id));
     };
    
  6. In the form element, add an onSubmit action that points to handleAddTodo

     <form className="App-form" onSubmit={handleAddTodo}>
    
  7. To show the list of to-dos, map the todos list to the TodoItem component and pass the id to the key and id attributes. In addition, pass the text of the todo and the handleTodoDone function.

    If the count is equal to 0, “No todos” will be displayed to the user.

     <div className="Todos">
             {count > 0 &&
               todos.map((todo) => (
                 <TodoItem
                   key={todo.id}
                   text={todo.text}
                   id={todo.id}
                   onCheck={handleTodoDone}
                 />
               ))}
             {count === 0 && <p>No todos</p>}
     </div>
    
  8. In the TodoItem file, add a function that listens to clicking on the item

    .

    Full TodoItem.js file:

     import "./TodoItem.css";
    
     const TodoItem = (props) => {
       const deleteTodo = () => {
         props.onCheck(props.id);
       };
       return (
         <div className="todo" onClick={deleteTodo}>
           <input type="checkbox"></input>
           <label>{props.text}</label>
         </div>
       );
     };
    
     export default TodoItem;
    
  9. Your app.js file should look like this now:

     import { useState } from "react";
    
     import "./App.css";
    
     import TodoItem from "./components/TodoItem";
     import { useSelector, useDispatch } from "react-redux";
     import { addTodo, removeTodo } from "./features/todoSlice";
    
     function App() {
       const [input, setInput] = useState("");
    
       const count = useSelector((state) => state.todo.count);
       const todos = useSelector((state) => state.todo.todos);
       const dispatch = useDispatch();
    
       const handleAddTodo = (e) => {
         e.preventDefault();
         dispatch(addTodo(input));
       };
    
       const handleTodoDone = (id) => {
         dispatch(removeTodo(id));
       };
       return (
         <div className="App">
           <h1>TODO List</h1>
           <form className="App-form" onSubmit={handleAddTodo}>
             <input type="text" onInput={(e) => setInput(e.target.value)} />
             <button type="submit">+</button>
           </form>
           <div className="Todos">
             {count > 0 &&
               todos.map((todo) => (
                 <TodoItem
                   key={todo.id}
                   text={todo.text}
                   id={todo.id}
                   onCheck={handleTodoDone}
                 />
               ))}
             {count === 0 && <p>No todos</p>}
           </div>
         </div>
       );
     }
    
     export default App;
    

Final Output

output.gif

Conclusion

That’s it for the redux toolkit tutorial, you can find the full code here. I personally prefer redux toolkit over regular redux because it lessens the boiler plate code a lot. No one likes to write the same code over and over again right? I hope you enjoyed this tutorial, your feedback is more than welcome! In the next tutorial, I’ll be connecting this to-do list app to firebase so stay tuned!