Redux provides flexibility to manage the state at the application level. We have already seen the basic implementation of Redux in React Js. Once, you create the state using Redux, it can be used in any of your components in the entire app. Today, by using Redux, we will create a React Todo App. But for data and state management, we will use Redux. So, by the end of this post, you will be learning to create a Todo list in React JS using Redux. Here, we will not use APIs for creating, and managing the Todo list. For that, I will come up with another post. In this post, we will dive into Redux state management.
We are going to create this advanced Todo App.
Prerequisites
Before starting this app, I am assuming, you are familiar with the basics of React js. For creating the React application, you required Node js and npm to be installed in your system.
Create React Todo App
Open the terminal, and hit the below command to have a new app in React js.
npx create-react-app todo-app
Once, the application has been created, you can try to run this.
npm start
It will jump to the default browser automatically.
Now, let’s install a couple of libraries in our Todo app. Firstly, starting with Bootstrap.
How to Use Axios in React JS For API Requests Handling
Install Bootstrap in React Js
For the UI design of the Todo app, I will be using Bootstrap. You can use other CSS frameworks as per your requirements.
npm i bootstrap
After installing CSS, we will be installing the Redux library in our app.
Install Redux in React Js
To install Redux, you have to hit the below command in the terminal.
npm i redux
To connect Redux with the React application, you will have to install one more library.
Install React Redux in React Js
This library will work as a connector with React and Redux.
npm install react-redux
Once the library setup is done, you will have the updated package.json file. So, you can verify the installed libraries.
Now, let’s create a couple of components in our React todo app.
Form Handling in React JS Using React Hook Form Library
Create Components For React Todo App
At the very step, create a new folder inside the src named components. Now, inside the components folder, let’s create the below components.
- AddTodo.jsx
- TodoLists.jsx
After creating the above components, let’s do the setup of the Redux for our app.
Redux Setup For React Todo App
Inside the src folder, create another folder named redux. Now, inside the redux folder, create three more folders. The folders will be –
- actions
- reducers and
- store
We have already seen the Redux setup and folder structure in our React Redux Counter App.
Now, let’s create action types and reducers for the Todo app. I will show you the folder structure after creating the files. So, it will be easy for you to understand.
The folder structure will look as shown below.
src
|__ components
| |_____ AddTodo.jsx
| |
| |_____ TodoLists.jsx
|
|__ redux
| |_____ actions
| | |_____ actionTypes.js
| | |
| | |_____ index.js
| |
| |_____ reducers
| | |_____ todoReducer.js
| | |
| | |_____ index.js
| |
| |_____ store
| |_____ index.js
|
|
Now, let’s start by adding actions for the Todo functionality.
Add Todo Action Types in Redux
Open the actionTypes.js file and add the below actions.
export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';
export const UPDATE_TODO = 'UPDATE_TODO';
export const EDIT_TODO = 'EDIT_TODO';
export const CLEAR_ALL_TODO = 'CLEAR_ALL_TODO';
export const MARK_COMPLETED = 'MARK_COMPLETED';
Next, we will use these action types, and based on these, we will perform todo actions.
import {
ADD_TODO,
DELETE_TODO,
CLEAR_ALL_TODO,
EDIT_TODO,
UPDATE_TODO,
MARK_COMPLETED,
} from "./actionTypes";
export const addNewTodo = (todo) => {
return {
type: ADD_TODO,
payload: {
id: Date.now(),
title: todo?.title,
description: todo?.description,
},
};
};
export const deleteTodo = (id) => {
return {
type: DELETE_TODO,
id,
};
};
export const clearAlltodo = () => {
return {
type: CLEAR_ALL_TODO,
};
};
export const editTodo = (id) => {
return {
type: EDIT_TODO,
payload: {
id: id,
},
isEdit: true,
};
};
export const updateTodo = (id, todo) => {
return {
type: UPDATE_TODO,
payload: {
todoId: id,
todoTitle: todo?.title,
todoDescription: todo?.description,
},
};
};
export const markTodoCompleted = (id) => {
return {
type: MARK_COMPLETED,
payload: {
selectedTodoId: id
}
}
}
Next, we will add reducers for performing todo actions based on the given payload.
API Handling in React Functional Component Using Hook
Add Reducer For Todo App
Navigate to the reducers/todoReducer.js file. Then add the below snippet.
import {
ADD_TODO,
DELETE_TODO,
CLEAR_ALL_TODO,
EDIT_TODO,
UPDATE_TODO,
MARK_COMPLETED,
} from "../actions/actionTypes";
const initialState = {
todos: [
{
id: 1,
title: "TodoList 1",
description: "This is first todo",
isCompleted: true,
isPending: false,
},
{
id: 2,
title: "TodoList 2",
description: "This is second todo",
isCompleted: false,
isPending: true,
},
{
id: 3,
title: "TodoList 3",
description: "This is third todo",
isCompleted: false,
isPending: true,
},
],
isEdit: false,
editTodoId: "",
};
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
const { id, title, description } = action.payload;
return {
...state,
todos: [
...state.todos,
{
id: id,
title: title,
description: description,
isCompleted: false,
isPending: true,
},
],
isEdit: action.isEdit,
};
case DELETE_TODO:
const newTodoList = state.todos.filter((item) => item.id != action.id);
return {
...state,
todos: newTodoList,
};
case EDIT_TODO:
const editTodo = action.payload;
let newEditTodo = state?.todos?.find((item) => item?.id === editTodo?.id);
return {
...state,
isEdit: action.isEdit,
editTodo: newEditTodo,
};
case UPDATE_TODO:
const { todoId, todoTitle, todoDescription } = action.payload;
const todos = state.todos.filter((todo) => {
return todo.id !== todoId;
});
const todo = state.todos.find((todo) => todo?.id === todoId);
todo.title = todoTitle;
todo.description = todoDescription;
todo.isCompleted = todo?.isCompleted;
todo.isPending = todo?.isPending;
todos.push(todo);
return {
...state,
todos: [...todos],
isEdit: false,
};
case MARK_COMPLETED:
const { selectedTodoId } = action.payload;
let allTodos = [];
selectedTodoId.forEach((id) => {
allTodos = state.todos.filter((todo) => {
return todo.id !== id;
});
const selectedTodo = state.todos.find((todo) => todo?.id === id);
selectedTodo.title = selectedTodo?.title;
selectedTodo.description = selectedTodo?.description;
selectedTodo.isCompleted = true;
selectedTodo.isPending = selectedTodo?.isPending;
allTodos.push(selectedTodo);
});
return {
...state,
todos: [...allTodos],
isEdit: false,
};
case CLEAR_ALL_TODO:
return {
...state,
todos: [],
};
default:
return state;
}
};
export default todoReducer;
At the very top of the reducer, I have created the initial state and in the initial state, created a few todos in the form of an array of objects.
Now, inside the todoReducer function, I have added the switch case to perform the action based on the given payload and action type.
Register Todo Reducer
For registering todoReducer.js file, you have to go to the reducers/index.js file. This file is for combing all reducers together. So, put the below snippet inside it.
import { combineReducers } from "redux";
import todoReducer from './todoReducer'
export default combineReducers({
todoReducer,
})
Next, we have to setup the root reducer to create the Redux store.
Add Todo Store
Open the store/index.js file and add the below lines.
import { createStore } from "redux";
import rootReducer from '../reducers'
const store = createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
export default store
Now, this store will be imported into the index.js of our Todo application.
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import store from "./redux/store";
import { Provider } from "react-redux";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
Create Todo App Functionality
We already created the components. So, in the AddTodo.jsx, put the below snippet.
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addNewTodo, updateTodo } from "../redux/actions";
export const AddTodo = () => {
const [value, setValue] = useState({});
const [error, setError] = useState("");
const dispatch = useDispatch();
const isEdit = useSelector((state) => state.todoReducer.isEdit);
const editTodo = useSelector((state) => state.todoReducer.editTodo);
useEffect(() => {
editTodo && setValue(() => editTodo);
}, [editTodo]);
const onSubmit = (e) => {
e.preventDefault();
if (!value?.title) {
setError((error) => ({
...error,
title: 'Please enter todo title',
}));
return;
}
if (!value?.description) {
setError((error) => ({
...error,
description: 'Please enter todo description'
}));
return;
}
if (isEdit) {
dispatch(updateTodo(editTodo.id, value));
}
else {
dispatch(addNewTodo(value));
}
setValue({title: '', description: ''});
document.getElementById("todoForm").reset();
};
const changeEvent = (e) => {
setValue(
{
...value,
[e.target.name]: e.target.value,
},
);
if (e?.target?.name === "title") {
setError({
title: "",
});
}
if (e?.target?.name === "description") {
setError({
description: ""
});
}
};
return (
<div className="container my-4 py-1 border">
<form className="mt-3 mb-2" id="todoForm" onSubmit={onSubmit}>
<div className="row">
<div className="col-xl-3">
<label className="sr-only">Name</label>
<input
type="text"
name="title"
className="form-control mb-2 mr-sm-3"
placeholder="Todo Title"
defaultValue={value?.title}
onChange={(e) => changeEvent(e)}
/>
<span className="text-danger">{error?.title}</span>
</div>
<div className="col-xl-3">
<label className="sr-only">Description</label>
<input
type="text"
name="description"
className="form-control mb-2 mr-sm-3"
placeholder="Description"
defaultValue={value?.description}
onChange={(e) => changeEvent(e)}
/>
<span className="text-danger">{error?.description}</span>
</div>
<div className="col-xl-2">
<button className="btn btn-primary mb-2" type="submit"> {isEdit ? 'Update Todo' : 'Create Todo'} </button>
</div>
</div>
</form>
</div>
);
};
Next, we have the TodoLists.jsx component, in which we will list out the created Todo.
How to Use useEffect Hook in React Functional Component
Show Todo Lists
Add the below snippet in TodoLists.jsx component.
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { deleteTodo, editTodo, markTodoCompleted, clearAlltodo } from "../redux/actions";
export const TodoLists = () => {
const todos = useSelector((state) => state.todoReducer.todos);
const dispatch = useDispatch();
const [selectedTodo, setSelectedTodo] = useState([]);
const actionClick = (data) => {
if (data && data?.type === "edit") {
dispatch(editTodo(data?.todo?.id));
} else if (data && data?.type === "delete") {
dispatch(deleteTodo(data?.todo?.id));
}
};
const changeEvent = (e, todoId) => {
if (e?.target?.name !== "select_all_todo" && e?.target?.checked === true) {
if (selectedTodo.indexOf(todoId) === -1) {
setSelectedTodo((todo) => [...todo, todoId]);
}
} else if (e?.target?.name !== "select_all_todo" && e?.target?.checked === false) {
const todos = selectedTodo.filter((todo) => todo !== todoId);
setSelectedTodo(todos);
}
if (e?.target?.name === "select_all_todo" && e?.target?.checked === true) {
todos && todos.forEach((todo, index) => {
const allChkbox = document.getElementsByName(`todo_${index}`);
for (let chk of allChkbox) {
chk.checked = true;
let todoId = todo?.id;
setSelectedTodo((todo) => [
...todo,
todoId
]);
}
});
}
else if (e?.target?.name === "select_all_todo" && e?.target?.checked === false) {
todos && todos.forEach((todo, index) => {
const allChkbox = document.getElementsByName(`todo_${index}`);
for (let chk of allChkbox) {
chk.checked = false;
setSelectedTodo([]);
}
});
}
};
const markCompleted = () => {
dispatch(markTodoCompleted(selectedTodo));
};
return (
<div className="container my-2">
<div className="row pb-4" style={{height: "60px"}}>
<div className="col-xl-12 text-right">
{selectedTodo.length > 0 && (
<>
<button
className="btn btn-danger"
onClick={() => dispatch(clearAlltodo())}
>
Clear Todos
</button>
<button
className="btn btn-success ml-2"
onClick={markCompleted}
>
Mark As Completed
</button>
</>
)}
</div>
</div>
<table className="table table-bordered">
<thead>
<tr>
<th width="3%">
<input
type={"checkbox"}
onChange={(e) => changeEvent(e)}
name={"select_all_todo"}
/>
</th>
<th width="30%">Name</th>
<th width="42%">Description</th>
<th width="8%">Status</th>
<th width="20%">Action</th>
</tr>
</thead>
<tbody>
{todos && todos.map((todo, index) => (
<tr key={index}>
<td>
<input
type={"checkbox"}
value={todo?.id}
onChange={(e) => changeEvent(e, todo?.id)}
name={`todo_${index}`}
/>
</td>
<td>{todo?.title}</td>
<td>{todo?.description}</td>
<td>
{todo?.isCompleted ? (
<span className="badge badge-success p-2">Completed</span>
) : todo?.isPending ? (
<span className="badge badge-danger p-2">Pending</span>
) : (
""
)}
</td>
<td>
<button
className="btn btn-primary btn-sm"
onClick={() => actionClick({ todo: todo, type: "edit" })}
>
Edit
</button>
<button
className="btn btn-danger btn-sm ml-1"
onClick={() => actionClick({ todo: todo, type: "delete" })}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
In the above snippet, we have listed out all todo lists from the Reducer. Also, we have the actions for Edit and Delete. Also, we have two more buttons at the top right.
If you want to mark as completed to any todo, then simply select that using the checkbox. Then click on Mark as Completed. The selected todo status will be changed to Completed.
Lastly, you have to import these two components into the App component.
import React from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import { AddTodo } from './components/AddTodo';
import { TodoLists } from './components/TodoLists';
const App = () => {
return (
<div className="container p-4 mt-2">
<h2>Todo Application</h2>
<AddTodo />
<TodoLists />
</div>
);
}
export default App;
Now, the Todo App is ready to run.
Todo App Results
I have added basic validation for required fields for the todo title and description.
Similarly, the validation error will appear for todo description as well.
As soon as you will start typing, the validation error message will be gone.
Once, todo is created, it will list out in the table automatically.
Edit/Update Todo
On click on the Edit, the todo will be passed in the input for the update.
Make any changes to see the effect.
Once, you have updated the Todo, it will show the updated list in the table.
Similarly, you can Delete a single todo or delete all by clicking on the Clear All button.
Final Words
We created a Todo app in React js using Redux. So, here we have created an initial state for displaying a few todos by default. On adding a new Todo, it will append to the initial state array and list out in the table. Similarly, if you want to edit, update, and delete then all actions will be performed from Reducer itself. Here, for storing and managing todo, we haven’t integrated any APIs. As I know, for a beginner, it will be difficult to understand Redux. So, in the coming post, I will show you the APIs integration for this Todo App.
Ramya S says
Very detailed explanation. Thanks for sharing it.
Vova says
Link is not working
Chris Alex says
Thanks, Well explained and Well Structured.