A todo app is one of the simplest applications you can create, but it can still teach you a lot about React. In this post, we will build a basic todo app with React using functional components, hooks, and state management. The Project consists of 5 sections (6 if you count the file preparation) and in the end of each section I have provided you with a link to a GitHub repo with different branches.Eaxh branch represents every step along the way. The files we'll be working with are located in the src folder.
At any point you can refer to my code on GitHub, or navigate directly to the master branch to check out the entire project 😉
0. React set up and file preparation:
Let's start by creating our app using the cra(create-react-app) tool. In the terminal run npx create-react-app react-todo-list
. Then cd react-todo-list
and last npm start
. More information can be found on react official page
https://beta.reactjs.org/learn/start-a-new-react-project
Continue with cleaning up the App.js and Index.js files. they should look like this:
// ----------- App.js file ---------------------------
import React, { Component } from 'react'
import './App.css';
export default class App extends Component {
render() {
return (
<div>
<App/>
</div>
)
}
}
// ------------ index.js file ---------------------------
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
Make a TodoList.js file with a ul
element. Add Constructor(props) to manage State in the file. At this point its an empty array:
this.state = { todos: [] }
Make a todo.js component that returns a div
with an <li>{this.props.task}<li>
and 2 buttons for “edit” and “delete”. The ‘task’ will be the argument that we are going to work with inside every Todo component we are rendering. It can be named anything e.g. YellowNutCracker, but its a good practise to name our variables and files according to their role/function.
Import the Todo.js file into TodoList.js and add sample data in the state, passing data as JSX:
`this.state = {todos:[task:”Feed the cat”]}`
Now its time to access the data (each task in Todo
component) from our state
and push it to the array. I like to use the array.map()
method for this that creates a new array populated with the results of calling a provided function on every element in the calling array. Then we ‘ll return a Todo React Component with a task property
:
const todos = this.state.todos.map(t ⇒ {
return <Todo task={t.task} />;
});
Shortly after, render the {todos}
array inside our <ul>
element. Use JSX syntax (duh!)
<ul> {todos} </ul>
At this point your code should look like this:
https://github.com/Harry2gks/react-todo-list-v2/tree/0.-Setup/src
1. Building the Form
We want the user to add, edit and delete his todos from our app. For that we can build a <form>
to accept the user inputs. Lets start by creating the Form.js file. Type rcc to generate the basic template if you have the extension enabled in Vs code, otherwise I reccoment to enable the extension to make your life easier. Create a Constructor(props) function to control the state in this Component and set task as an empty string, since it will accept the user input as a string
this.state = {task: “”}`
`<form>
<label htmlFor=’task’> </label>
<input
type=”text”
placeholder=”New Todo”
id=”task”
name="task"
value={this.state.task}
onChange = {this.handleChange} // controls what the user can change
/>`
`<button>Add Todo</button>
</form>
Now it is time for our best friend handleChange()
method. Note that this is a custom method, not a standart JS method, like array.map()
`handleChange(evt) {
this.setState({
[evt.target.name] : evt.target.value // from the “name” : change the “value”
});`
and don’t forget to bind our method to the component in the Constructor:
this.handleChange = this.handleChange.bind(this);
2. Adding new Todo / Create functionality
Let’s add the Create functionality from C.R.U.D. .For that we are going to make a new method create()
that is going to accept an object as a parameter and push it in [todos]
array inside our state. Thats why we are going to build it inside our TodoList.js file:
create(newTodo) {
this.setState({
todos: […this.state.todos, newTodo] // Take the array from state and add newTodo object
});
}
and bind it:
this.create = this.create.bind(this);
Then we need to pass our method as a property inside the <Form>
we are rendering here, in our TodoList.js file
<Form createTodo={this.create} />
Now we have the method create()
inside our Form.js file as prop called createTodo, and will call it inside the handleSubmit()
, passing the state as parameter. The handleSubmit()
will be called when the user submits the form, thus creating a new <Todo>
and will push the user input as a task to the state. Then we’ll clear the form:
`handleSubmit(evt) {
evt.preventDefault();
this.props.createTodo(this.state);
this.setState({ task:”” })
}`
and bind it:
this.handleSubmit = this.handleSubmit.bind()this;
At this point the code should look like this:
https://github.com/Harry2gks/react-todo-list-v2/tree/C-from-C.R.U.D
3. Adding the Delete functionallity
Deleting is similar to creating, we’will make a delete()
function inside our TodoList.js file using the array.filter()
method. We ‘ll also pass it as a prop inside our render()
, so that we’ll be able to call it inside the handleDelete()
function in our Todo.js file. At this point I would like to point out that every <Todo>
we’ re rendering requires a react {key}
and also an {id}
since we'll be deleting using the id. I like to use a library that helps me create different ids for every item called uuid : https://www.npmjs.com/package/uuid
`delete(id) {
this.setState({
todos: this.state.todos.filter((t) => t.id !==id),
});
}`
`<Todo
key={t.id}
id={t.id}
task={t.task}
deleteTodo={this.delete} // delete() passed as a prop inside render()
/>;`
`handleDelete() {
this.props.deleteTodo(this.props.id)
}`
and bind it.
Now let’s add the handleDelete()
to our button:
<button onClick={this.handleDelete}>Delete</button>
You code should look like this now:
https://github.com/Harry2gks/react-todo-list-v2/tree/D-from-C.R.U.D-/src
4. Editing / Updating Todo
This part is a bit tricky. In order to edit the we’ll need to add some conditinal logic in our Todo.js file to display the editing form when the user is editing or the list of todos when the user is not.
In our state add the boolean logic isEditing: false
and borrow the task from the todo:
this.state = {
isEditing: false,
task: this.props.task
}
We’ll also make a custom method toggleForm()
that is going to display or hide the form as mentioned above. And of course we ‘ll need to create a form:
render() {
let edit;
if (this.state.isEditing) {
edit = (
<form onSubmit={this.handleSubmit}>
<input
type="text"
name="task"
value={this.state.task}
onChange={this.handleChange}/>
<button>Update</button>
</form>
);
} else {
edit = (
<div>
<button onClick={this.toggleForm}{>Edit</button>
<button onClick={this.handleRemove}>Delete</button>
</div>
)
}
return edit;
}
As we’ve seen before we need our best friends handleChange()
and handleSubmit()
just like in the Form.js file and our toggleForm()
connected with an onClick
event to our button.
handleChange(evt) {
this.setState({
[evt.target.name]: evt.target.value
});
}
handleSubmit(evt) {
evt.preventDefault();
// take the new task Data and pass it to the parent (todoList)
this.props.updateTodo(this.props.id, this.state.task);
this.setState({ isEditing: false });
}
toggleForm() {
this.setState({ isEditing: !this.state.isEditing });
}
and bind in Constructor
. I know, I Know.
Next we need to take the updated task and pass it up to the parent component TodoList.js. To do that we’ll need to define a method and know 2 things: what are we updating -->id<-- and the new task -->updatedTodo<--:
update(id, updatedTask) {
const updatedTodos = this.state.todos.map((todo) => {
if (todo.id === id) {
return { ...todo, task: updatedTask };
}
return todo;
});
this.setState({ todos: updatedTodos });
}
return todo;
});
this.setState({ todos: updatedTodos });
}
and pass the methods to our render inside the
<Todo
key={t.id}
id={t.id}
task={t.task}
removeTodo={this.remove}
updateTodo={this.update}
/>
at this point your code should look like this:
https://github.com/Harry2gks/react-todo-list-v2/tree/U-from-C.R.U.D-
Section 5 --> Mark Todo as “completed”
Lets make sure our Todos can keep track of when they are completed or not. In our Form.js file, when we make a new todo (createNewTodo
) we’ll add completed:false
boolean logic to handleSubmit method:
handleSubmit(evt) {
evt.preventDefault();
this.props.createNewTodo({ ...this.state, id: uuidv4(), completed: false });
this.setState({ task: "" });
}
Then add a conditional className in our <li>
that displays the task inside the Todo.js:
<li className={this.props.completed ? "completed" : ""}>
and create a .completed
class in our App.css:
.completed {
colour: gray,
text-decoration: line-through
}
and pass it down in our TodoList when we render the Todo component
render() {
const todos = this.state.todos.map((t) => {
return <Todo
key={t.id}
id={t.id}
task={t.task}
completed={t.completed} // <------------
removeTodo={this.remove}
updateTodo={this.update}
/>;
});
Following the same pattern with edit, we are going to add a button that toggles the form, calls a method in the parent element and updates the state. In our TodoList.js file:
toggleCompleted(id) {
const updatedTodos = this.state.todos.map((t) => {
if (t.id === id) {
return { ...t, completed: !t.completed };
}
return t;
});
this.setState({ todos: updatedTodos });
this.toggleCompleted = this.toggleCompleted.bind(this); // up top
and then pass it down as a prop to our :
toggleTodo={this.toggleCompleted}
.
Since we’re making the <li>
clickable, then we ‘ll require an onClick event and our handleChange()
method, with a different name handleToggle().
in Todo.js file:
<li className={this.props.completed ? "completed" : ""}
onClick={this.handleToggle}>
{this.props.task}</li>
handleToggle(evt) {
this.props.toggleTodo(this.props.id);
}
this.handleToggle = this.handleToggle.bind(this); // up top
Code up to here:
https://github.com/Harry2gks/react-todo-list-v2/tree/Toggle-Completed-Todos/src
This article is starting to get a bit long, so i am gonna wrap it up with some styling.
Give the
className=”container” and add the following styles into App.css, or copy my stylesheet directly from github.
.completed{
color: gray;
text-decoration: line-through;
}
.container {
display: block;
width: 500px;
margin: 10px auto 100px;
background-color: #cc9797;
padding: 0px 10px 10px 10px;
border-radius: 10px;
text-align: center;
color: #7b0e0e;
font-size: 18px;
}
ul {
list-style: none;
}
li {
margin: 10px;
font-size: 22px;
}
li:hover {
cursor: pointer;
}
ul button {
padding: 2px 2px 2px 2px;
margin: 2px 4px 2px 4px;
font-size: 18px;
}
button {
background: #7b0e0e;
background-image: -webkit-linear-gradient(top, #7b0e0e, #7a0e40);
background-image: -moz-linear-gradient(top, #7b0e0e, #7a0e40);
background-image: -ms-linear-gradient(top, #7b0e0e, #7a0e40);
background-image: -o-linear-gradient(top, #7b0e0e, #7a0e40);
background-image: linear-gradient(to bottom, #7b0e0e, #7a0e40);
-webkit-border-radius: 8;
-moz-border-radius: 8;
border-radius: 8px;
-webkit-box-shadow: 0px 1px 3px #524d52;
-moz-box-shadow: 0px 1px 3px #524d52;
box-shadow: 0px 1px 3px #524d52;
font-family: Arial;
color: #ebbcbc;
font-size: 20px;
padding: 4px 4px 4px 4px;
text-decoration: none;
}
button:hover {
background: #c91818;
background-image: -webkit-linear-gradient(top, #c91818, #610c34);
background-image: -moz-linear-gradient(top, #c91818, #610c34);
background-image: -ms-linear-gradient(top, #c91818, #610c34);
background-image: -o-linear-gradient(top, #c91818, #610c34);
background-image: linear-gradient(to bottom, #c91818, #610c34);
text-decoration: none;
}
input {
padding: 5px;
margin: 0px 20px;
border-radius: 8px;
background-color: #e7bcbc;
color: #7b0e0e;
}
Our lovely project is done! (finally)
Critisism accepted and appreciated :)
Live Project: https://react-todo-list-v2-lemon.vercel.app/
GitHub https://github.com/Harry2gks
Top comments (0)