In this series, we will build a todo application.
To begin, we will go over a very basic way to build this application and revise as we gain more knowledge.
I suggest following along and if you get stuck, you can fork the code from the Code Sandbox.
In the previous posts, added the ability to delete todos. Now lets add the edit functionality.
1. Add more state
We need to add a couple more state values we can use to help us get the editing functionality.
// boolean state to know if we are editing (this will let us display
// different inputs based on a condition (conditional rendering)
const [isEditing, setIsEditing] = useState(false);
// object state to set so we know which todo item we are editing
const [currentTodo, setCurrentTodo] = useState({});
2. Setup onChange handler for edit input
Now lets setup an onChange handler for the new input we will create in the next step.
// function to get the value of the edit input and set the new state
function handleEditInputChange(e) {
// set the new state value to what's currently in the edit input box
setCurrentTodo({ ...currentTodo, text: e.target.value });
console.log(currentTodo);
}
3. Re-format JSX
We are going to make quite a few changes in the JSX. I'll break this down:
- We need to check if we are in editing mode
- If we are editing, then display the editing form
- If we are not editing, then display the add todo form
- The editing form has a couple additional buttons so the user can control what they would like to do
return (
<div className="App">
{/* We need to conditionally render different inputs based on if we are in editing mode */}
{isEditing ? (
// if we are editing - display the edit todo input
// make sure to add the handleEditFormSubmit function in the "onSubmit" prop
<form onSubmit={handleEditFormSubmit}>
{/* we've added an h2 element */}
<h2>Edit Todo</h2>
{/* also added a label for the input */}
<label htmlFor="editTodo">Edit todo: </label>
{/* notice that the value for the update input is set to the currentTodo state */}
{/* also notice the handleEditInputChange is being used */}
<input
name="editTodo"
type="text"
placeholder="Edit todo"
value={currentTodo.text}
onChange={handleEditInputChange}
/>
{/* here we added an "update" button element - use the type="submit" on the button which will still submit the form when clicked using the handleEditFormSubmit function */}
<button type="submit">Update</button>
{/* here we added a "Cancel" button to set isEditing state back to false which will cancel editing mode */}
<button onClick={() => setIsEditing(false)}>Cancel</button>
</form>
) : (
// if we are not editing - display the add todo input
// make sure to add the handleFormSubmit function in the "onSubmit" prop
<form onSubmit={handleFormSubmit}>
{/* we've added an h2 element */}
<h2>Add Todo</h2>
{/* also added a label for the input */}
<label htmlFor="todo">Add todo: </label>
{/* notice that the value is still set to the todo state */}
{/* also notice the handleInputChange is still the same */}
<input
name="todo"
type="text"
placeholder="Create a new todo"
value={todo}
onChange={handleInputChange}
/>
{/* here we just added a "Add" button element - use the type="submit" on the button which will still submit the form when clicked using the handleFormSubmit function */}
<button type="submit">Add</button>
</form>
)}
<ul className="todo-list">
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
{/* we are passing the entire todo object to the handleEditClick function*/}
<button onClick={() => handleEditClick(todo)}>Edit</button>
<button onClick={() => handleDeleteClick(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
After adding the JSX, you should see the interface has changed some.
3. Handle when a user clicks "Edit" button
There are a few functions you see in the JSX above that we need to make in order to make this function.
Lets start with when a user clicks on the "Edit" button. We want the "Update" button and "Cancel" button, and change from the add todo input to the editing input.
// function to handle when the "Edit" button is clicked
function handleEditClick(todo) {
// set editing to true
setIsEditing(true);
// set the currentTodo to the todo item that was clicked
setCurrentTodo({ ...todo });
}
// for this to work, do not forget to use the handleEditClick function in the JSX - when its clicked we will pass the todo object as an argument
<ul className="todo-list">
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
{/* we are passing the entire todo object to the handleEditClick function*/}
<button onClick={() => handleEditClick(todo)}>Edit</button>
<button onClick={() => handleDeleteClick(todo.id)}>Delete</button>
</li>
))}
</ul>
Now we should see that the currentTodo is the todo we clicked on, which is getting us close. Notice how the todo text is now in the editing input.
4. Adding the updated text to the todos state
Here we will create a function that we will call when the form is submitted.
// function to edit a todo item
function handleUpdateTodo(id, updatedTodo) {
// here we are mapping over the todos array - the idea is check if the todo.id matches the id we pass into the function
// if the id's match, use the second parameter to pass in the updated todo object
// otherwise just use old todo
const updatedItem = todos.map((todo) => {
return todo.id === id ? updatedTodo : todo;
});
// set editing to false because this function will be used inside a onSubmit function - which means the data was submited and we are no longer editing
setIsEditing(false);
// update the todos state with the updated todo
setTodos(updatedItem);
}
5. Call the handleUpdateTodo function
Last step is to actually update the todo item when the form is submitted.
function handleEditFormSubmit(e) {
e.preventDefault();
// call the handleUpdateTodo function - passing the currentTodo.id and the currentTodo object as arguments
handleUpdateTodo(currentTodo.id, currentTodo);
}
6. Putting it all together
After doing everything mentioned above, you should have something that looks like this:
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [todos, setTodos] = useState(() => {
const savedTodos = localStorage.getItem("todos");
if (savedTodos) {
return JSON.parse(savedTodos);
} else {
return [];
}
});
const [todo, setTodo] = useState("");
// boolean state to know if we are editing (this will let us display
// different inputs based on a condition (conditional rendering)
const [isEditing, setIsEditing] = useState(false);
// object state to set so we know which todo item we are editing
const [currentTodo, setCurrentTodo] = useState({});
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);
function handleInputChange(e) {
setTodo(e.target.value);
}
// function to get the value of the edit input and set the new state
function handleEditInputChange(e) {
// set the new state value to what's currently in the edit input box
setCurrentTodo({ ...currentTodo, text: e.target.value });
console.log(currentTodo);
}
function handleFormSubmit(e) {
e.preventDefault();
if (todo !== "") {
setTodos([
...todos,
{
id: todos.length + 1,
text: todo.trim()
}
]);
}
setTodo("");
}
function handleEditFormSubmit(e) {
e.preventDefault();
handleUpdateTodo(currentTodo.id, currentTodo);
}
function handleDeleteClick(id) {
const removeItem = todos.filter((todo) => {
return todo.id !== id;
});
setTodos(removeItem);
}
// function to edit a todo item
function handleUpdateTodo(id, updatedTodo) {
// here we are mapping over the todos array - the idea is check if the todo.id matches the id we pass into the function
// if the id's match, use the second parameter to pass in the updated todo object
// otherwise just use old todo
const updatedItem = todos.map((todo) => {
return todo.id === id ? updatedTodo : todo;
});
// set editing to false because this function will be used inside a onSubmit function - which means the data was submited and we are no longer editing
setIsEditing(false);
// update the todos state with the updated todo
setTodos(updatedItem);
}
// function to handle when the "Edit" button is clicked
function handleEditClick(todo) {
// set editing to true
setIsEditing(true);
// set the currentTodo to the todo item that was clicked
setCurrentTodo({ ...todo });
}
return (
<div className="App">
{/* We need to conditionally render different inputs based on if we are in editing mode */}
{isEditing ? (
// if we are editing - display the edit todo input
// make sure to add the handleEditFormSubmit function in the "onSubmit" prop
<form onSubmit={handleEditFormSubmit}>
{/* we've added an h2 element */}
<h2>Edit Todo</h2>
{/* also added a label for the input */}
<label htmlFor="editTodo">Edit todo: </label>
{/* notice that the value for the update input is set to the currentTodo state */}
{/* also notice the handleEditInputChange is being used */}
<input
name="editTodo"
type="text"
placeholder="Edit todo"
value={currentTodo.text}
onChange={handleEditInputChange}
/>
{/* here we added an "update" button element - use the type="submit" on the button which will still submit the form when clicked using the handleEditFormSubmit function */}
<button type="submit">Update</button>
{/* here we added a "Cancel" button to set isEditing state back to false which will cancel editing mode */}
<button onClick={() => setIsEditing(false)}>Cancel</button>
</form>
) : (
// if we are not editing - display the add todo input
// make sure to add the handleFormSubmit function in the "onSubmit" prop
<form onSubmit={handleFormSubmit}>
{/* we've added an h2 element */}
<h2>Add Todo</h2>
{/* also added a label for the input */}
<label htmlFor="todo">Add todo: </label>
{/* notice that the value is still set to the todo state */}
{/* also notice the handleInputChange is still the same */}
<input
name="todo"
type="text"
placeholder="Create a new todo"
value={todo}
onChange={handleInputChange}
/>
{/* here we just added a "Add" button element - use the type="submit" on the button which will still submit the form when clicked using the handleFormSubmit function */}
<button type="submit">Add</button>
</form>
)}
<ul className="todo-list">
{todos.map((todo) => (
<li key={todo.id}>
{todo.text}
{/* we are passing the entire todo object to the handleEditClick function*/}
<button onClick={() => handleEditClick(todo)}>Edit</button>
<button onClick={() => handleDeleteClick(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
You should be able to change the todo item text now!
This is the fourth post in this series. Keep in mind that in this post, we added the ability to edit a todo item in the app. We now have full CRUD functionality. However, I think this could be structured better. We will look at re-factoring the code into separate components in the next post.
Thanks for reading!
Top comments (5)
Integrated tested , all works just fine, appreciated ! There is one little, but... : let's say if you created three items e.g. No.1 , No.2, No3 as list items and you suddenly decided to e.g. remove No.2 , remaining indices continue to be sparsed (non-dynamic) which results into duplicate index whenever the next list time added . Fortunately there are solution or alternative way to solve the issue :
As I saw I got a heart on this recently , I'd like to thank the author that saved my a*se passing my final examination on MERN stack . I am just about to integrate it with Heroku so will edit comment in the future to share my work with www . If not Joseph not sure I would made it . Thank you ! <3
Try this for handleDeleteClick function:
const DeleteTask=[...todos];
DeleteTask.splice(id,1);
setTodos(DeleteTask);
Hey cool intro!
This is fine for an introduction and sample of what react does, but this could be simplified by creating additional components. You can one component doing many things.
@tilkinsc I completely agree. In the next post I will be refactoring by creating additional components. Trying to keep things simple for an introduction, but it's getting messy. Thanks for the feedback!