Build a Fabulous Todo List App with React, Redux and Framer-Motion
Hi there,
I know building TODO List always won’t get you too far😴, But It can teach you basic concepts and implementation of particular framework. Here is the demo of what we’re going to build in this tutorial.👇
Demo Link: Awesome React Redux Todo List App
What will you learn from this tutorial?
-
How to use Redux Toolkit
- How to use Framer-Motion for awesome animations
- Method to sort and display lists
- CRUD operations (obviously🤭)
If you prefer to code along with the same steps while listing to music you can watch this video 👀:
You must have basic understanding of Redux to follow this tutorial, don’t worry if you don’t know the basics of Redux you can visit my channel, there is playlist to learn redux.
Setting Up The Project And Installing Dependencies
Here is the folder structure that we are going to use for this small project so make sure to create it.
Write following commands to create react-app and install required libraries!
npx create-react-app your-app-name
cd your-app-name
npm install react-redux @reduxjs/toolkit framer-motion react-icons
We’re going to use react-icons
to add svg files in our app. Let’s add one input and add button in the Todos.js
.
import React, { useState } from "react";
const Todos = (props) => {
const [todo, setTodo] = useState("");
const handleChange = (e) => {
setTodo(e.target.value);
};
return (
<div className="addTodos">
<input
type="text"
onChange={(e) => handleChange(e)}
className="todo-input"
value={todo}
/>
<button className="add-btn">
Add
</button>
<br />
</div>
);
};
export default Todos;
As you can see in above code it has one input with handleChange()
method and one add button
.
Creating a Reducer And Store
Now let’s create Reducer and actions for this application. Open reducer.js
file and write the following code:
1import { createSlice } from "@reduxjs/toolkit";
2
3const initialState = [];
4
5const addTodoReducer = createSlice({
6 name: "todos",
7
8 initialState,
9
10 reducers: {
11 //here we will write our reducer
12 //Adding todos
13 addTodos: (state, action) => {
14 state.push(action.payload);
15 return state;
16 },
17
18 },
19
20});
21
22export const { addTodos } = addTodoReducer.actions;
23export const reducer = addTodoReducer.reducer;
24
Now here we’re going to use createSlice() function. This function takes 1 object having 3 parameters,
- name of the slice function
- initial State
- All reducer logic inside reducers object
Line 1: Importing the createSlice
function.
Line 2: creating initial state, it is an empty array.
Line 5: Here we have used createSlice
function and passed all 3 required parametrs.
Line 13: We are creating one action called addTodos
this action get an callback function which have two arguments ( state
, action
). Then this function will return state with adding action.payload
(payload contains one todo item).
Line 22: Here we are exporting addTodos
as action from addTodoReducer
.
Line 23: In this line we are exporting reducer
from addTodoReducer
.
Creating a Store
Now, let’s create store and pass this reducer. Open store.js
and write the following code:
import { configureStore } from "@reduxjs/toolkit";
import { reducer } from "./reducer";
const store = configureStore({
reducer: reducer,
});
export default store;
In the above code we have used configureStore function which takes one reducer
and automatically takes care for the Redux DevTools extension so we don’t have to worry about it. Now the store
is ready with reducer
that we have created.
Connecting Redux Store With React App
Let’s connect this store to our React application. I like to connect store
in the index.js
file. Open the index.js
file and import Provider
from the react-redux
and store
from store.js
file.
import { Provider } from "react-redux";
import store from "./redux/store";
Now just wrap your <App/>
component with this Provider
and pass store
in the Provider
as the following code block,
ReactDOM.render(
<React.StrictMode>
//Just like below 👇
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
Now our Redux store is connected with our React App.
Connect React component with Redux
Todos.js
component. To connect this component with Redux we will use connect() method from react-redux
. Open Todos.js
file and import connect
method from react-redux
.import { connect } from "react-redux";
Now instead of simple export default Todos
change it to the following line:
export default connect(null, null)(Todos);
connect
method, It’s like higher order function that takes your component (Todos in our case) and add redux functionalities to it then return it.Now add props in your component and log this props. You will see an Object having
dispatch
method. Which means your component is now connected with Redux.
Let’s use todos state in our component. To use state from Redux we have to pass mapStateToProps
method in the connect
method and to use actions or functions that we created inside the reducer (like addTodos
) we have to create and pass mapDispatchToProps
method and add it to the coonect method. So let’s create both of this methods in the Todos
component.
const mapStateToProps = (state) => {
return {
todos: state,
};
};
This method takes state as argument and will return state as we want. Here, I want state as todos
.
const mapDispatchToProps = (dispatch) => {
return {
addTodo: (obj) => dispatch(addTodos(obj)),
};
};
This method takes dispatch
as argument and it can dispatch action to reducer
. Here, I want to add todos so this method returns an addTodo
method. The addTodo
method dispatch an addTodos
action with an obj (which contains todo item, it will acts as action.payload).
Here, make sure to import
addTodos
action fromreducer
file.
Now add both of these methods in the connect just like the following code block,
export default connect(mapStateToProps, mapDispatchToProps)(Todos);
let’s connect input and add button with this state and methods.
1import React, { useState } from "react";
2import { connect } from "react-redux";
3import { addTodos } from "../redux/reducer";
4
5const mapStateToProps = (state) => {
6 return {
7
8 todos: state,
9
10 };
11};
12
13const mapDispatchToProps = (dispatch) => {
14 return {
15
16 addTodo: (obj) => dispatch(addTodos(obj)),
17
18 };
19};
20
21const Todos = (props) => {
22
23 console.log("props", props);
24
25 const [todo, setTodo] = useState("");
26
27 const add = () => {
28 if (todo === "") {
29 alert("Input is Empty");
30 } else {
31 props.addTodo({
32 id: Math.floor(Math.random() * 1000),
33 item: todo,
34 completed: false,
35 });
36 setTodo("");
37 }
38
39 };
40
41 const handleChange = (e) => {
42
43 setTodo(e.target.value);
44
45 };
46
47 return (
48
49 <div className="addTodos">
50 <input
51 type="text"
52 onChange={(e) => handleChange(e)}
53 className="todo-input"
54 value={todo}
55 />
56
57 <button className="add-btn" onClick={() => add()}>
58 Add
59 </button>
60 <br />
61
62 <ul>
63 {props.todos.length > 0 &&
64 props.todos.map((item) => {
65 return <li key={item.id}>{item.item}</li>;
66 })}
67 </ul>
68
69 </div>
70
71 );
72};
73
74//we can use connect method to connect this component with redux store
75export default connect(mapStateToProps, mapDispatchToProps)(Todos);
76
77
Line 23: Here I have created add
function. First it will check if todo state is not empty if it is empty then shows an alert else it will use addTodo
method from props. In this method we will pass todo object which contains id
, todo
text, completed
boolean which is initially false.
Line 50: Make sure to connect add()
with onClick
of button.
Line 55: here I have mapped values from todos
state. If todos.length > 0
then it will map it and shows all todo items you add.
You can also use Redux DevTools Extension to see actions and state.
Adding All Operations in The Reducer
Let’s add all the required operations in the reducer
.
1import { createSlice } from "@reduxjs/toolkit";
2
3const initialState = [];
4
5const addTodoReducer = createSlice({
6 name: "todos",
7 initialState,
8 reducers: {
9 //here we will write our reducer
10 //Adding todos
11 addTodos: (state, action) => {
12 state.push(action.payload);
13 return state;
14 },
15 //remove todos
16 removeTodos: (state, action) => {
17 return state.filter((item) => item.id !== action.payload);
18 },
19 //update todos
20 updateTodos: (state, action) => {
21 return state.map((todo) => {
22 if (todo.id === action.payload.id) {
23 return {
24 ...todo,
25 item: action.payload.item,
26 };
27 }
28 return todo;
29 });
30 },
31 //completed
32 completeTodos: (state, action) => {
33 return state.map((todo) => {
34 if (todo.id === action.payload) {
35 return {
36 ...todo,
37 completed: true,
38 };
39 }
40 return todo;
41 });
42 },
43 },
44});
45
46export const {
47 addTodos,
48 removeTodos,
49 updateTodos,
50 completeTodos,
51} = addTodoReducer.actions;
52export const reducer = addTodoReducer.reducer;
53
Line 16: Here, the removeTodos
will filterout items whose id is same as action.payload
. (which means while using this action we will pass id as payload)
Line 20: The updateTodos
is used to change todo text or todo.item
. It will check if id is same as passed in action.payload
then it will return all other properties of the item and change the text of todos with the passed value.
Line 32: The completeTodos
will change the completed boolean value of particular item to true.
Line 46: Make sure to export all the required todo actions.
Now we will use all these actions. Let’s separate display todos component from Todos.js
file. Remove ul
list from it and let’s add it in the DisplayTodo
item component. Before creating DisplayTodos.js
component, first let’s create TodoItem.js
component. so, open TodoItem.js
file and write the following code.
Don’t read this code, First read the explanation.
1import React, { useRef } from "react";
2import { AiFillEdit } from "react-icons/ai";
3import { IoCheckmarkDoneSharp, IoClose } from "react-icons/io5";
4
5const TodoItem = (props) => {
6
7 const { item, updateTodo, removeTodo, completeTodo } = props;
8
9 const inputRef = useRef(true);
10
11 const changeFocus = () => {
12
13 inputRef.current.disabled = false;
14 inputRef.current.focus();
15
16 };
17
18 const update = (id, value, e) => {
19
20 if (e.which === 13) {
21 //here 13 is key code for enter key
22 updateTodo({ id, item: value });
23 inputRef.current.disabled = true;
24 }
25
26 };
27 return (
28
29 <li
30 key={item.id}
31 className="card"
32 >
33 <textarea
34 ref={inputRef}
35 disabled={inputRef}
36 defaultValue={item.item}
37 onKeyPress={(e) => update(item.id, inputRef.current.value, e)}
38 />
39 <div className="btns">
40 <button onClick={() => changeFocus()}>
41 <AiFillEdit />
42 </button>
43 {item.completed === false && (
44 <button
45 style={{ color: "green" }}
46 onClick={() => completeTodo(item.id)}
47 >
48 <IoCheckmarkDoneSharp />
49 </button>
50 )}
51 <button
52 style={{ color: "red" }}
53 onClick={() => removeTodo(item.id)} >
54
55 <IoClose />
56 </button>
57
58 </div>
59 {item.completed && <span className="completed">done</span>}
60
61 </li>
62
63 );
64};
65
66export default TodoItem;
67
68
Now as you saw in the demo each todo item contains 3 buttons edit,completed,delete. and 3 methods connected with these buttons.
Line 2 & 3: This contains import of icons from react-icons
library, we will use this icons in edit, update and remove buttons.
Line 7: These are the all required items that we have to pass while displaying TodoItem
component.
item
: contains all the data of single todo item.updateTodo
: Method to update todo.completeTodo
: method to set todo is completed.removeTodo
: method to remove todo item.
Line 23: Here in the return inside this li
element you can see,
textarea
: it shows the todo text as default value.buttons
: after text area there are 3 buttons which contains icons for edit, update and remove, this buttons are connected with all required methods.span
: after these buttons there is one span element which shows done, and it will display only whenitem.completed
is true.
Line 9: It is a ref which is connected with textarea
. It’s value is true.
Line 30: Here, we have used ref
value for the disabled attribute, which means while ref is true until then textarea
stays disabled.
Line 11: This change
function will enable the textarea
and adds focus on it. This function is connected with the edit button.
Line 16: This function is used to update value of the todo item. It will take 3 arguments, id, updated value and event. Then when you press the enter key then it will call the updateTodo
method and pass all required things as object and disable the textarea. It is connected on onKeyPress
in the textarea at Line 32.
Line 48: This remove button is connected with the remove
method. we have to pass id of the item we want to remove in this method.
Now let’s use this component inside the DisplayTodos.js
file. Open DisplayTodos.js
and write the following code.
1import React, { useState } from "react";
2import { connect } from "react-redux";
3import {
4 addTodos,
5 completeTodos,
6 removeTodos,
7 updateTodos,
8} from "../redux/reducer";
9import TodoItem from "./TodoItem";
10
11const mapStateToProps = (state) => {
12 return {
13 todos: state,
14 };
15};
16
17const mapDispatchToProps = (dispatch) => {
18 return {
19 addTodo: (obj) => dispatch(addTodos(obj)),
20 removeTodo: (id) => dispatch(removeTodos(id)),
21 updateTodo: (obj) => dispatch(updateTodos(obj)),
22 completeTodo: (id) => dispatch(completeTodos(id)),
23 };
24};
25
26const DisplayTodos = (props) => {
27 const [sort, setSort] = useState("active");
28 return (
29 <div className="displaytodos">
30 <div className="buttons">
31
32 <button
33 onClick={() => setSort("active")}
34 >
35 Active
36 </button>
37
38 <button
39 onClick={() => setSort("completed")}
40 >
41 Completed
42 </button>
43
44 <button
45 onClick={() => setSort("all")}
46 >
47 All
48 </button>
49
50 </div>
51
52 <ul>
53
54 {props.todos.length > 0 && sort === "active"
55 ? props.todos.map((item) => {
56 return (
57 item.completed === false && (
58
59 <TodoItem
60 key={item.id}
61 item={item}
62 removeTodo={props.removeTodo}
63 updateTodo={props.updateTodo}
64 completeTodo={props.completeTodo}
65 />
66
67 )
68 );
69 })
70 : null}
71 {/* for completed items */}
72 {props.todos.length > 0 && sort === "completed"
73 ? props.todos.map((item) => {
74 return (
75 item.completed === true && (
76 <TodoItem
77 key={item.id}
78 item={item}
79 removeTodo={props.removeTodo}
80 updateTodo={props.updateTodo}
81 completeTodo={props.completeTodo}
82 />
83 )
84 );
85 })
86 : null}
87 {/* for all items */}
88 {props.todos.length > 0 && sort === "all"
89 ? props.todos.map((item) => {
90 return (
91 <TodoItem
92 key={item.id}
93 item={item}
94 removeTodo={props.removeTodo}
95 updateTodo={props.updateTodo}
96 completeTodo={props.completeTodo}
97 />
98 );
99 })
100 : null}
101
102 </ul>
103 </div>
104 );
105};
106
107export default connect(mapStateToProps, mapDispatchToProps)(DisplayTodos);
108
Make sure to import
DisplayTodos.js
component in theApp.js
file right after theTodos
component.
Line 1-9: Contains all the requried imports.
Line 12 & 18: we have already discussed about both of these method. Both of these methods must be passed in the connect method. One of them is to map state to props while the other method contains all the required methods to dispatch particular actions.
Line 28: This state is for those 3 buttons which are active, completed and all. It is initialised with active.
Line 31: This div contains all 3 buttons and onClick
of these buttons sort
state gets changed based on the button it’s values can be active, completed or all.
Line 53: In this ul
element we’re rendering 3 different lists based on conditions like,
- Renders active todo items when (
item.completed
===false
) and (sort
=== “active”) - Renders completed todo items when (
item.completed
=== true) and (sort
=== “completed”) - Renders all todo items when (
sort
=== “all”)
Line 61-65: This contains all the data that we need to pass in the TodoItem
component.
Full Code of this Tutorial👇
Build Awesome Todo App using React Redux