← Back to list

ToDo App with React+MUI/Bun+Vite, Part I

bless2k@gmail.com Jan 21,2025

HyunWoo Lee, Yet Another Software Engineer · 2025-01-26 15:14 · 50 claps · 5.5 min read
#bűn #react #vitejs #mui #typescript
Open on Medium ↗
Wiki topics: 🌐 · Web Development

ToDo App with React+MUI/Bun+Vite, Part I

bless2k@gmail.com Jan 21,2025

1. Intro

In this article, I will introduce a process to help developers who are using React for the first time to build a frontend. The good thing about strarting a new project is that you’re not stuck with old methods and can try out the latest technologes. The technology stack used in this article includes the following:

  • Bun v1.2 A fast all-in-one JavaScript runtime that includes a built-in package manager, bundler, and test runner. It is designed as an alternative to Node.js and its associated tools, such as Deno and Yarn.
  • Vite A modern frontend build tool that provides fast dev. and optimized production builds using ES modules.
  • React(+TypeScript) v19
  • Material UI(MUI) v6.4.x An open-source React component library that implements Google’s Material Design.

2. Project Setup

First, you need to install Bun, Visit it’s homepage for installation instructions and install it on your system:

# for mac and linux
$ curl -fsSL https://bun.sh/install | bash

Now, refer to the following page to initialize a new Vite + React project, select a ‘React’ framework and a ‘TypeScript’ language. Then you have a new project created in the ‘react-todo-app’ folder and can test it immediately.

[embed]Build a frontend using Vite and Bun | Bun Examples While Vite currently works with Bun, it has not been heavily optimized, nor has Vite been adapted to use Bun's bundler…bun.sh

$ bun create vite react-todo-app

With BunX, you can run this project. Bunx is a tool provided by Bun that allows you to run commands from within your Bun environment, similar to how npx works in Node.js.

$ cd react-todo-app
$ bun install
$ bunx --bun vite

Now, Open the url ‘http://localhost:5173/’. What you see is what you expected.

Once you’ve confirmed that it’s working, modify the app’s configuration file, ‘package.json’, as follows, and then check if it runs correctly in you IDE such as vscode, intellij, etc. Note that this Bun+Vite framework used here supports hot-reload, so in mose cases, there’s no need to restart the server during development.

{
  ...
  "scripts": {
    "dev": "bunx --bun vite",
    "build": "bunx vite build",
    "lint": "eslint ."
  },
  ...
}

Next, install the MUI component library. It provides a wide range of pre-built, customizable components like buttons, grids, modals, and navigation elements, making it easier to build modern web UI. For mui v5.0 and above, add it as follows:

$ bun install @mui/material @emotion/react @emotion/styled

3. Create a ToDo list app

Search the internet for a sample todo list project written in React + Typescript, and try migrating it to match our current settings. Here are some recommended tutorials.

Now, follow the guide to create the sample ToDo app. First, delete all existing in the ‘src’ folder and then create new files as shown below.

  • src/main.tsx
  • src/App.tsx
  • src/components/ToDoList.tsx

[Image] Project Structure

[Image] Project Structure

It maybe somewhat chaellinging for beginners, but if you use the tutorial’s source and see the following screen, it’s a success.

[Image] ToDo List App

[Image] ToDo List App

Now, the app is functional now, but since we’ve already installed the MUI component library(‘@mui/material’), there’s no need to use basic components. Replacing them with MUI components will give a Material Design look UI. Here’s the code:

import { useState } from 'react';
import { Button, TextField, List, ListItem, ListItemText, Checkbox} from '@mui/material';

interface TodoItem {
    id: string;
    text: string;
    completed: boolean;
}

const ToDoList = () => {
    const [todos, setTodos] = useState<TodoItem[]>([]);
    const [newTodo, setNewTodo] = useState('');

    const addTodo = () => {
        if (newTodo !== '') {
            const newId = crypto.randomUUID();
            const newTodoItem: TodoItem = {
                id: newId,
                text: newTodo,
                completed: false,
            };
            setTodos([...todos, newTodoItem]);
            setNewTodo('');
        }
    };

    const removeTodo = (id: string) => {
        const updatedTodos = todos.filter((todo) => todo.id !== id);
        setTodos(updatedTodos);
    };

    const toggleComplete = (id: string) => {
        const updatedTodos = todos.map((todo) => {
            if (todo.id === id) {
                return { ...todo, completed: !todo.completed };
            }
            return todo;
        });
        setTodos(updatedTodos);
    };

    return (
        <div>
            <h1>Todo App</h1>
            <div style={{ display: 'flex', gap: '10px' }}>
                <TextField
                    label="New Todo"
                    variant="outlined"
                    value={newTodo}
                    onChange={(e) => setNewTodo(e.target.value)}
                    fullWidth
                />
                <Button variant="contained" color="primary" onClick={addTodo}>
                    Add
                </Button>
            </div>
            <List>
                {todos.map((todo) => (
                    <ListItem key={todo.id} secondaryAction={
                        <Button onClick={() => removeTodo(todo.id)} color="secondary" size="small">
                            Delete
                        </Button>
                    }>
                        <Checkbox
                            checked={todo.completed}
                            onChange={() => toggleComplete(todo.id)}
                            color="primary"
                        />
                        <ListItemText
                            primary={todo.text}
                            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
                        />
                    </ListItem>
                ))}
            </List>
        </div>
    );
};

export default ToDoList;

[Image] ToDo List App with MUI components

[Image] ToDo List App with MUI components

4. Refined Code

Although the optimal way to divide UI components may not always be clear, the previous code can be further broken down into smaller components as shown below.

// components/TodoItem.tsx
interface TodoItem {
    id: string;
    text: string;
    completed: boolean;
}

export default TodoItem
// components/TodoList.tsx
import TodoItem from './TodoItem.tsx';
import { Button, List, ListItem, ListItemText, Checkbox } from '@mui/material';
import React from 'react';

interface TodoListProps {
    todoItemList: TodoItem[];
    onRemove: (id: string) => void;
    onToggle: (id: string) => void;
}

const TodoList: React.FC<TodoListProps> = ({ todoItemList, onRemove, onToggle }) => {
    return (
        <List>
            {todoItemList.map((todo) => (
                <ListItem
                    key={todo.id}
                    secondaryAction={
                        <Button onClick={() => onRemove(todo.id)} color="secondary" size="small">
                            Delete
                        </Button>
                    }
                >
                    <Checkbox
                        checked={todo.completed}
                        onChange={() => onToggle(todo.id)}
                        color="primary"
                    />
                    <ListItemText
                        primary={todo.text}
                        sx={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
                    />
                </ListItem>
            ))}
        </List>
    );
};

export default TodoList;
// App.tsx
import TodoItem from './components/TodoItem.tsx';
import TodoList from './components/TodoList.tsx';
import { Button, Container, TextField, Typography, Box } from '@mui/material';
import { useState } from 'react';

function App() {
    const [todoItemList, setTodoItemList] = useState<TodoItem[]>([]);
    const [newTodo, setNewTodo] = useState('');

    const addTodo = () => {
        if (!newTodo.trim()) return;

        const newTodoItem: TodoItem = {
            id: crypto.randomUUID(),
            text: newTodo,
            completed: false,
        };
        setTodoItemList((prevTodos) => [...prevTodos, newTodoItem]);
        setNewTodo('');
    };

    const removeTodo = (id: string) => {
        setTodoItemList((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
    };

    const toggleComplete = (id: string) => {
        setTodoItemList((prevTodos) =>
            prevTodos.map((todo) =>
                todo.id === id ? { ...todo, completed: !todo.completed } : todo
            )
        );
    };

    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={addTodo}>
                    Add
                </Button>
            </Box>

            <TodoList todoItemList={todoItemList} onRemove={removeTodo} onToggle={toggleComplete} />
        </Container>
    );
}

export default App;

5. More(?) Refined Code

In the previous example, the todoItemList is shared between two components: the "Add Button" and the "TodoList". In this case, separating the business logic related to todoItemList can improve readability and maintainability. For this purpose, you can create a new custom hook or module (e.g., useTodoList.ts) that handles all state manipulation.

// components/useTodoList.tsx
import { useState } from 'react';
import TodoItem from "./TodoItem.tsx"

export const useTodoList = () => {
    const [todoItemList, setTodoItemList] = useState<TodoItem[]>([]);

    const addTodo = (newTodo: string) => {
        if (!newTodo.trim()) return;
        const newTodoItem: TodoItem = {
            id: crypto.randomUUID(),
            text: newTodo,
            completed: false,
        };
        setTodoItemList((prevTodos) => [...prevTodos, newTodoItem]);
    };

    const removeTodo = (id: string) => {
        setTodoItemList((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
    };

    const toggleComplete = (id: string) => {
        setTodoItemList((prevTodos) =>
            prevTodos.map((todo) =>
                todo.id === id ? { ...todo, completed: !todo.completed } : todo
            )
        );
    };

    return {
        todoItemList,
        addTodo,
        removeTodo,
        toggleComplete,
    };
};

Summary

React has been a widely used framework for a long time, making it a well-established choice in web development. However, frontend technologies continue to evolve rapidly, and staying updated with the latest trends is essential. Bun, Vite, and MUI are part of an excellent React development ecosystem, enhancing development convenience. As shown in this article, Bun + Vite + React + TypeScript + MUI is a highly efficient and productive stack.


메타데이터
post_id
e77afd94567d
slug
todo-list-app-with-bun-react-part-1-e77afd94567d
url
https://medium.com/@bless2k/todo-list-app-with-bun-react-part-1-e77afd94567d
canonical_url
https://medium.com/@bless2k/todo-list-app-with-bun-react-part-1-e77afd94567d
author_url
https://medium.com/@bless2k
status
ok
fetched_at
2026-06-26 12:24:55