ToDo App with React+MUI/Bun+Vite, Part II
bless2k@gmail.com Jan 21,2025
ToDo App with React+MUI/Bun+Vite, Part II
bless2k@gmail.com Jan 21,2025
This article explains React development with the latest tech stack and continues from the previous post.
Once you understand the basic structure and operation of React, the next step is learning how to integrate with backend services. For web applications, using the REST approach offers several advantages. REST APIs are readable, easy to debug, and well-supported by tools like fetch, express framework, and Postman. Additionally, JavaScript and TypeScript provide built-in support for JSON, making it easy to handle data exchange. This allows most tasks to be performed seamlessly with minimal effort.
1. Start Refactoring
The project structure remains similar to the previous one. Refer to the following image for the location of the upcoming source code.
![[Image] Project Folder Structure](https://miro.medium.com/v2/resize:fit:1206/1*HxXBbfCsKeofpZSXkL8ixg.png)
[Image] Project Folder Structure
The package.json file contents are as follows, with minimal changes from the previous version. This is because React natively provides functionality for integrating with REST APIs.
{
"name": "react-todo-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "bunx --bun vite",
"build": "tsc -b && vite build",
"lint": "eslint ."
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.1",
"@mui/material": "^6.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
...
}
2. useTodoList Hook
‘useTodoList.ts’ defines a custom React hook (useTodoList) that manages the state and actions for interacting with a Todo list REST API using fetch. A React hook is a function that allows you to use React features like state, side effects, and context in function components, simplifying the code and enabling better reusability and separation of concerns. There are various types of hooks and usage examples, but a detailed explanation will be omitted here. In our code, the primary hook used is the useState and useEffect hooks.
**useState**Used to manage state in the component. It initializes the state variables (todoList,loading, anderror) and provides functions (setTodoItemList,setLoading, andsetError) to update these states.**useEffect** Used for side effects such as fetching data from the API when the component mounts. It runs thefetchTodoListfunction once when the component is first rendered.
// components/useTodoList.tsx
import { useEffect, useState } from 'react';
const API_BASE = "http://localhost:8080/todo";
interface Todo {
id: string;
text: string;
isCompleted: boolean;
}
export const useTodoList = () => {
const [todoList, setTodoItemList] = useState<Todo[]>([])
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchTodoList = async () => {
try {
const resp = await fetch(API_BASE);
if (!resp.ok) throw new Error("@ Error Fetch todoList")
const data: Todo[] = await resp.json();
setTodoItemList(data)
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
fetchTodoList();
}, []);
const addTodo = async (newTodo: string): Promise<void> => {
if (!newTodo) return;
setLoading(true);
try {
const resp = await fetch(API_BASE, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ text: newTodo, completed: false })
});
if (!resp.ok) throw new Error("@ Failed to post new Todo");
const addedTodo: Todo = await resp.json();
setTodoItemList((prevTodos: Todo[]) => [...prevTodos, addedTodo]);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
const removeTodo = async (id: string) => {
setLoading(true);
try {
const resp = await fetch(`${API_BASE}/${id}`, {
method: "DELETE",
});
if (!resp.ok) throw new Error("@ Failed to delete Todo");
setTodoItemList( (prevTodos) => prevTodos.filter( (todo) => todo.id != id));
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
const toggleCompleted = async (id: string) => {
setLoading(true);
try {
const todo = todoList.find( (t) => t.id === id);
if (!todo) return;
const resp = await fetch(`${API_BASE}/${id}`, {
method: "PATCH",
headers: {"Content-Type": "application/json"},
body: JSON.stringify( { completed: !todo.isCompleted }),
});
if (!resp.ok) throw new Error(`@ Failed to toggle Completed: ${id}`);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
return {
todoList,
loading,
error,
addTodo,
removeTodo,
toggleCompleted,
};
};
export default Todo
As seen in the code, all REST API interactions are asynchronous and return a Promise. This allows the operation to be non-blocking, enabling the UI to remain responsive while waiting for the server’s response. This is a common approach in JavaScript, especially for handling asynchronous operations like network requests, timers, or reading/writing files.
3. UI Codes
The remaining UI code does not change significantly, as the tasks handled internally are now managed through the react hook in useTodoList.tsx.
// components/TodoList.tsx
import Todo from './useTodoList.tsx';
import { Button, List, ListItem, ListItemText, Checkbox } from '@mui/material';
import * as React from 'react';
interface TodoListProps {
todoList: Todo[];
onRemove: (id: string) => void;
onToggle: (id: string) => void;
}
const TodoList: React.FC<TodoListProps> = ({ todoList, onRemove, onToggle }) => {
return (
<List>
{todoList.map((todo) => (
<ListItem
key={todo.id}
secondaryAction={
<Button onClick={() => onRemove(todo.id)} color="secondary" size="small">
Delete
</Button>
}
>
<Checkbox
checked={todo.isCompleted}
onChange={() => onToggle(todo.id)}
color="primary"
/>
<ListItemText
primary={todo.text}
sx={{ textDecoration: todo.isCompleted ? 'line-through' : 'none' }}
/>
</ListItem>
))}
</List>
);
};
export default TodoList;
// App.tsx
import {useTodoList} from './components/useTodoList.tsx';
import TodoList from './components/TodoList.tsx';
import {useState} from "react";
import { Container, Typography, Box, TextField, Button, CircularProgress, Alert, Dialog, DialogContent } from "@mui/material";
const App: React.FC = () => {
const { todoList, loading, error, addTodo, removeTodo, toggleCompleted } = useTodoList();
const [newTodo, setNewTodo] = useState("");
const handleAddTodo = () => {
if (!newTodo.trim()) return;
addTodo(newTodo);
setNewTodo("");
};
return (
<Container maxWidth="sm">
<Typography variant="h4" align="center" gutterBottom>
Todo App with React + MUI
</Typography>
<Box display="flex" gap={2} mb={3}>
<TextField
label="New Todo"
variant="outlined"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
fullWidth
/>
<Button variant="contained" color="primary" onClick={handleAddTodo}>
Add
</Button>
</Box>
{error && <Alert severity="error">{error}</Alert>}
<TodoList todoList={todoList} onRemove={removeTodo} onToggle={toggleCompleted} />
{/* Loading Popup */}
<Dialog open={loading} aria-labelledby="loading-dialog">
<DialogContent sx={{ display: "flex", justifyContent: "center", alignItems: "center", p: 3 }}>
<CircularProgress />
</DialogContent>
</Dialog>
</Container>
);
}
export default App;
4. REST API Server
Finally, let’s create the server that provides the REST API. In the case of React, the web client and the REST API server can be developed completely separtely, and therefor, it’s not necessary to use JavaScript or TypeScript for it; any method for providing the REST API can be used. There are various REST API frameworks available for different languages, such as Java’s Spring, Python’s Flask, and JavaScript’s Express.
Here is an example using the Go Echo framework for the REST API. The API provided by this server is as follows, and it can be tested using API testing tools like Postman:
- GET /todo fetch all todo list
- POST /todo
- DELETE /todo/{id} delte todo if ‘id’
- PATCH /todo/{id} patch todo.isCompleted value
One important point to note when creating a REST API server is that you need to disable CORS (Cross-Origin Resource Sharing) security cause our frontend and backend can be running on different domains or ports. By default, browsers block cross-origin requests for security reasons, but in development, you should allow our frontend to make requests to the backend server.
the method for handling CORS differs across REST frameworks. In the case of Go Echo, enabling CORS is straightforward.
e.Use(middleware.CORS())
The full go server source code is as follows:
package main
import (
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"log"
"net/http"
"time"
)
type Todo struct {
ID string `json:"id"`
Text string `json:"text"`
Completed bool `json:"completed"`
}
var todoList []Todo = make([]Todo, 0)
func getTodoList(c echo.Context) error {
log.Printf("> get Todo List: %v\n", todoList)
time.Sleep(2 * time.Second)
return c.JSON(http.StatusOK, todoList)
}
func addTodo(c echo.Context) error {
var newTodo Todo
if err := c.Bind(&newTodo); err != nil {
return err
}
newTodo.ID = uuid.NewString()
log.Printf("> add Todo: %v\n", newTodo)
todoList = append(todoList, newTodo)
time.Sleep(2 * time.Second)
return c.JSON(http.StatusCreated, newTodo)
}
func removeTodo(c echo.Context) error {
id := c.Param("id")
for i, todo := range todoList {
if todo.ID == id {
todoList = append(todoList[:i], todoList[i+1:]...)
time.Sleep(2 * time.Second)
return c.NoContent(http.StatusNoContent)
}
}
return c.JSON(http.StatusNotFound, map[string]string{"error": "Todo not found"})
}
func toggleComplete(c echo.Context) error {
id := c.Param("id")
for i, todo := range todoList {
if todo.ID == id {
log.Printf("> toggle Todo: %v\n", todo)
todoList[i].Completed = !todoList[i].Completed
time.Sleep(2 * time.Second)
return c.JSON(http.StatusOK, todoList[i])
}
}
return c.JSON(http.StatusNotFound, map[string]string{"error": "Todo not found"})
}
func main() {
e := echo.New()
e.Use(middleware.CORS())
e.GET("/todo", getTodoList)
e.POST("/todo", addTodo)
e.DELETE("/todo/:id", removeTodo)
e.PATCH("/todo/:id", toggleComplete)
e.Logger.Fatal(e.Start(":8080"))
}
5. TEST IT
Now it’s time to test the actual functionality. First, start the server and launch the React client. When the app is in a loading state, a loading icon will be displayed as follows.
![[Image] Todo App in a loading state(loading == true)](https://miro.medium.com/v2/resize:fit:953/1*WV5RpJvbvYq8ZaKxnC-Ggw.png)
[Image] Todo App in a loading state(loading == true)
Since the server and client operate completely separately, testing can be done independently. For the client, you can use a dummy server to test the REST API integration, while for the server, you can write test cases based on specific scenarios. In simpler cases, testing can be done using tools like Postman. Additionally, modern browsers like Chrome allow you to view request/response messages and logs through their developer tools (DevTools, F12). This makes it easier to inspect API calls, monitor network activity, and debug issues directly from the browser.
![[Image] Test with Chrome DevTools](https://miro.medium.com/v2/resize:fit:1113/1*nMYg4IajkzRutyOjEpas0w.png)
[Image] Test with Chrome DevTools
Summary
React has been the most widely used frontend UI framework for over 10 years, so most of the tools and solutions you might need already exist. In fact, as a frontend application developer, the main task is simply to find and apply the most efficient and maintainable methods(solutions & tools). From this perspective, the React+MUI/Bun+Vite stack described in this article is a recommended combination.
메타데이터
- post_id
- e956ac5ecff0
- slug
- todo-app-with-react-mui-bun-vite-part-ii-e956ac5ecff0
- url
- https://medium.com/@bless2k/todo-app-with-react-mui-bun-vite-part-ii-e956ac5ecff0
- canonical_url
- https://medium.com/@bless2k/todo-app-with-react-mui-bun-vite-part-ii-e956ac5ecff0
- author_url
- https://medium.com/@bless2k
- status
- ok
- fetched_at
- 2026-06-17 12:55:42