This week I decided to go deeper into React with a small project. The knowledge required to do this was not vast by any means, but it was nice to move away from snippets and theory to do something more practical. While the project was technically full stack, in this post I'll be delving primarily into the frontend. For the backend, you can see the full code on my GitHub here. Let's get right into it.
The Project - a very basic todo application✅
The project was a simple todo application that consisted of both a backend and a frontend.
The backend 🌐
The backend had the following endpoints-
- POST /todos - allows the user to add a todo. The zod schema for this was
const createTodo = z.object({
title: z.string(),
description: z.string()
});
- GET /todos - allows the user to get all their existing todos.
- PUT /completed - allows the user to update an existing todo and mark it as done. The zod schema for this was -
const updateTodo = z.object({
id: z.string(),
});
As I already said, the backend is quite simple and we've already covered these topics in our previous blogs, but you can get the full code at my GitHub. Let's get into the frontend now!
The frontend 💡
Now this bit is interesting(gotten a little bored with the backend:). I initialised an empty vite project, cleared the boilerplate code and setup the App.jsx to have only a couple of things — the rest of the stuff would be structured away neatly for modularity and readability.
In the App.jsx I declared my todos as a state variable which would be updated everytime the site re-renders(when a new todo is added or an existing todo is marked as done). For the first render, we would use the useEffect hook to update todos.
The useEffect hook🪝 -
Let's see the syntax first.
useEffect(() => {
fetch("http://localhost:3000/todos")
.then(async (res) => {
const json = await res.json();
setTodos(json.todos);
});
}, []);
This makes it so that what's written in the function executes only when the React app mounts(or on specified conditions — we'll see that soon). This gives us great control over how many fetch requests we send out and on which re-renders the request runs.
This is controlled through the second argument in the useEffect hook which is called the dependency array. This array is where we pass in the dependencies which decide when the function in the first argument runs.
For eg. say I add a state variable 'a' to this dependency array. Then whenever 'a' updates, the first function runs. If we leave the dependency array empty then it gives the desired result in our case, which is the fetch request going out only on the app mount.
A couple of things to note here🚨-
- We can't make the function in the first argument async. This is because useEffect hook expects the first argument to be a function that returns either nothing or a cleanup function. But an async function always returns a promise.
- We should not put a state variable in the dependency array that is being updated in the function in the first argument as this would trigger an infinite loop.
The next part in the App.jsx was that it would return a div which would have two components as follows -
return (
<div>
<CreateTodo setTodos={setTodos} />
<Todos todos={todos} setTodos={setTodos} />
</div>
)
Let's delve into the components-
1. CreateTodo
I created the file 'CreateTodo.jsx' in the components folder in the src folder.
This was a relatively simple component which just returned a div with the input fields necessary to add a new todo.
import { useState } from "react";
export function CreateTodo({setTodos}) {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
return (<div>
<input type="text" placeholder="Title" onChange={(e) => {
const value = e.target.value;
setTitle(value);
}}></input><br />
<input type="text" placeholder="Description" onChange={(e) => {
const value = e.target.value;
setDescription(value);
}}></input><br />
<button onClick={() => {
fetch("http://localhost:3000/todos", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: title,
description: description
})
}).then(async (res) => {
fetch("http://localhost:3000/todos")
.then(async (res) => {
const json = await res.json();
setTodos(json.todos);
});
});
}}>Add a todo</button>
</div>);
}
Now this may seem jarring at first, but let's break it down.
- We have two state variables for each of the input fields which update whenever the user types in them. These are used to send the backend calls necessary to add a new todo.
- The button which has a pretty big onClick function which does two things. Firstly, it sends the POST request to add a new todo. You can see the syntax above to send the body with the request. After this, it asynchronously sends another request to GET the newly updated todos from the backend and updates the todos state variable. This causes a re-render in the
<Todos>component and updates the displayed todos.
2. Todos
This is the component-
export function Todos({todos , setTodos}){
return <div>
{todos.map((todo)=>{
return <div>
<h1>{todo.title}</h1>
<h1>{todo.description}</h1>
{todo.completed?<span>Completed</span>:<button onClick={()=>{
handleCompleted(todo._id,todos ,setTodos);
}}>Mark as done</button>}
</div>
})}
</div>
}
This is pretty simple and a very good use-case of the map method. It simply maps all the todos in the variable to be displayed individually in divs on the screen.
The main magic here is in the handleCompleted function.
function handleCompleted(id,todos,setTodos){
fetch("http://localhost:3000/completed",{
method:"PUT",
headers:{
"Content-Type":"application/json"
},
body: JSON.stringify({
id
})
})
.then (async(res)=>{
const modifiedTodo = await res.json();
const newTodos = todos.map((t) =>
t._id === modifiedTodo.updatedTodo._id ? modifiedTodo.updatedTodo : t
);
setTodos(newTodos);
})
}
This sends the PUT request to the endpoint to update the todo to mark it as done. Then it updates the todos to include the new updated todo instead of the old one (very good use-case of the ternary operator).
Some new things I learned🧨
- The useEffect hook - nature of its two arguments!
- How these backend calls do not actually hit the backend unless we include "CORS" in the backend which enables the backend to be hit from any URL.
- The syntax of sending these requests can seem a bit verbose and jarring but we simplify it by using the Axios library which we will study soon!
Wrapping up🔚
The practicality of this week's assignment kept me hooked. Now that we have some more understanding of React, we dive deeper into React next week. Some more hooks and smaller topics. If you have any questions or feedback, make sure to comment and let me know!
I'll be back next week with more. Until then, stay consistent!
Top comments (0)