🗒️

React と MUI で ToDo アプリを作ろう!

に公開

🎯 はじめに

本記事では、ReactMaterial UI(MUI) を使って、シンプルな ToDo リストアプリを一から構築するハンズオンを紹介します。まずは基本機能を実装し、その後でMUIを使ってデザインを整えていきます。

  • ✅ Reactの基本的な構成が学べる
  • ✅ 機能実装からスタイリングまでステップバイステップで解説
  • ✅ MUIによるスタイリングの実践

完成イメージはこちら(GitHub リポジトリ)👇

👉 https://github.com/Ash65536/todo-app/


🛠 開発環境のセットアップ

まずは React アプリを作成します。

npx create-react-app todo-app
cd todo-app

📁 プロジェクト構成

最終的に以下のような構成になります。まずは機能を実装し、後からMUIを導入します。

src/
├── App.js          # アプリのメインコンポーネント
├── TodoForm.js     # タスク入力フォーム
├── TodoList.js     # タスク一覧表示

🧩 Step 1: 基本機能の実装

App.js の作成

アプリの中心となるコンポーネントを作成します。

import React, { useState } from 'react';
import TodoForm from './TodoForm';
import TodoList from './TodoList';

function App() {
  const [todos, setTodos] = useState([]);

  // 新しいToDoを追加する関数
  const addTodo = (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    setTodos([...todos, newTodo]);
  };

  // ToDoの完了状態を切り替える関数
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // ToDoを削除する関数
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div className="app">
      <h1>ToDoリスト</h1>
      <TodoForm addTodo={addTodo} />
      <TodoList 
        todos={todos} 
        toggleTodo={toggleTodo} 
        deleteTodo={deleteTodo} 
      />
    </div>
  );
}

export default App;

TodoForm.js の作成

タスク入力用フォームコンポーネントを作成します。

import { useState } from "react";

const TodoForm = ({ addTodo }) => {
  const [text, setText] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim() === "") return;
    addTodo(text);
    setText("");
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="新しいタスクを入力"
      />
      <button type="submit">追加</button>
    </form>
  );
};

export default TodoForm;

TodoList.js の作成

タスク一覧を表示するコンポーネントを作成します。

const TodoList = ({ todos, toggleTodo, deleteTodo }) => {
  if (todos.length === 0) {
    return <p>タスクがありません。新しいタスクを追加してください。</p>;
  }

  return (
    <ul className="todo-list">
      {todos.map((todo) => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span 
            style={{
              textDecoration: todo.completed ? "line-through" : "none"
            }}
          >
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>削除</button>
        </li>
      ))}
    </ul>
  );
};

export default TodoList;

▶️ 基本機能の確認

この時点で開発サーバーを起動して、機能を確認してみましょう。

npm start

ブラウザで http://localhost:3000 にアクセスすると、基本機能を持ったToDoアプリが表示されます。

🎨 Step 2: MUIの導入とスタイリング

次にMaterial UIをインストールして、アプリにスタイルを適用していきます。

npm install @mui/material @mui/icons-material @emotion/react @emotion/styled

App.js の修正

MUIのコンポーネントを使ってスタイリングします。

import React, { useState } from 'react';
import { Container, Typography, Box } from '@mui/material';
import TodoForm from './TodoForm';
import TodoList from './TodoList';

function App() {
  const [todos, setTodos] = useState([]);

  // 新しいToDoを追加する関数
  const addTodo = (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    setTodos([...todos, newTodo]);
  };

  // ToDoの完了状態を切り替える関数
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // ToDoを削除する関数
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <Container maxWidth="sm" sx={{ mt: 4 }}>
      <Box sx={{ textAlign: 'center', mb: 4 }}>
        <Typography variant="h4" component="h1" gutterBottom>
          ToDoリスト
        </Typography>
      </Box>
      <TodoForm addTodo={addTodo} />
      <TodoList 
        todos={todos} 
        toggleTodo={toggleTodo} 
        deleteTodo={deleteTodo} 
      />
    </Container>
  );
}

export default App;

TodoForm.js の修正

フォームをMUIコンポーネントで修正します。

import { useState } from "react";
import { Box, Button, TextField } from "@mui/material";

const TodoForm = ({ addTodo }) => {
  const [text, setText] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim() === "") return;
    addTodo(text);
    setText("");
  };

  return (
    <Box component="form" onSubmit={handleSubmit} sx={{ display: "flex", mb: 3 }}>
      <TextField
        label="新しいタスクを入力"
        variant="outlined"
        value={text}
        onChange={(e) => setText(e.target.value)}
        fullWidth
        sx={{ mr: 2 }}
      />
      <Button 
        type="submit" 
        variant="contained" 
        color="primary"
        disabled={text.trim() === ""}
      >
        追加
      </Button>
    </Box>
  );
};

export default TodoForm;

TodoList.js の修正

タスク一覧をMUIでスタイリングします。

import { 
  List, 
  ListItem, 
  Checkbox, 
  IconButton, 
  ListItemText, 
  ListItemButton,
  ListItemIcon,
  Paper
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";

const TodoList = ({ todos, toggleTodo, deleteTodo }) => {
  if (todos.length === 0) {
    return <Paper sx={{ p: 2, textAlign: 'center' }}>タスクがありません。新しいタスクを追加してください。</Paper>;
  }

  return (
    <Paper elevation={2}>
      <List sx={{ width: '100%' }}>
        {todos.map((todo) => (
          <ListItem
            key={todo.id}
            secondaryAction={
              <IconButton 
                edge="end" 
                onClick={() => deleteTodo(todo.id)}
              >
                <DeleteIcon />
              </IconButton>
            }
            disablePadding
          >
            <ListItemButton onClick={() => toggleTodo(todo.id)} dense>
              <ListItemIcon>
                <Checkbox
                  edge="start"
                  checked={todo.completed}
                  tabIndex={-1}
                  disableRipple
                />
              </ListItemIcon>
              <ListItemText
                primary={todo.text}
                sx={{
                  textDecoration: todo.completed ? "line-through" : "none",
                  color: todo.completed ? "text.secondary" : "text.primary"
                }}
              />
            </ListItemButton>
          </ListItem>
        ))}
      </List>
    </Paper>
  );
};

export default TodoList;

⚙️ index.js の修正

MUIのテーマを適用するためにindex.jsを修正します。

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';

const theme = createTheme({
  palette: {
    primary: {
      main: '#2196f3',
    },
    secondary: {
      main: '#f50057',
    },
  },
});

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <App />
    </ThemeProvider>
  </React.StrictMode>
);

🚀 Step 3: 機能拡張

基本のToDoアプリが完成したら、さらに機能を拡張してみましょう。例として、ローカルストレージへの保存機能を実装します。

ローカルストレージへの保存機能

App.jsを修正して、タスクをローカルストレージに保存します。

import React, { useState, useEffect } from 'react';
import { Container, Typography, Box } from '@mui/material';
import TodoForm from './TodoForm';
import TodoList from './TodoList';

function App() {
  // ローカルストレージからタスクを読み込む
  const [todos, setTodos] = useState(() => {
    const savedTodos = localStorage.getItem('todos');
    return savedTodos ? JSON.parse(savedTodos) : [];
  });

  // タスクが変更されたらローカルストレージに保存
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  const addTodo = (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    setTodos([...todos, newTodo]);
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <Container maxWidth="sm" sx={{ mt: 4 }}>
      <Box sx={{ textAlign: 'center', mb: 4 }}>
        <Typography variant="h4" component="h1" gutterBottom>
          ToDoリスト
        </Typography>
      </Box>
      <TodoForm addTodo={addTodo} />
      <TodoList 
        todos={todos} 
        toggleTodo={toggleTodo} 
        deleteTodo={deleteTodo} 
      />
    </Container>
  );
}

export default App;

▶️ 完成したアプリを実行しよう!

開発サーバーを起動して、完成したToDoアプリを確認しましょう。

npm start

🔍 さらなる機能拡張のアイデア

このアプリをベースに、以下のような機能を追加してみるのもおすすめです:

  1. タスクの編集機能
  2. 日付や優先度の追加
  3. フィルタリング機能(完了/未完了の表示切替)
  4. タスクのカテゴリ分け
  5. ドラッグ&ドロップによる並べ替え
  6. ダークモード対応

🧑‍💻 GitHub リポジトリ

完全なコードは GitHub に公開しています👇

📎 https://github.com/Ash65536/todo-app

📝 おわりに

本記事では、Reactの基本機能を使ったToDoアプリの実装から、MUIを使ったスタイリングまでステップバイステップで解説しました。React の状態管理や、MUI のコンポーネントの活用方法を学ぶことができたと思います。これをベースに機能を拡張して、オリジナルの ToDo アプリを作成してみてください!

質問やフィードバックがあれば、コメント欄にお願いします。

Discussion