Udemyセール!最大95%オフ!1,200円~Udemy公式サイト >

この記事にはプロモーションが含まれています。

【React】useEffectと副作用・ローカルストレージ|TODOアプリ【基本編:第7回】

【React】useEffectと副作用・ローカルストレージ|TODOアプリ【基本編:第7回】

Reactで作るTODOアプリ開発シリーズ第7回(基本編 最終回)

いよいよ基本編の最終回となるこの記事では、

ReactのuseEffectフックを使った副作用処理ローカルストレージによるデータ永続化

について学びます。

前回編集・削除機能を追加し、基本的なCRUD操作を完成させました。今回は、TODOリストの保存や、実用的な副作用処理の基礎を身につけていきます。

これにより、アプリのデータを永続化ができるようになります。

この記事でわかること
  • useEffectフックの基本的な使い方
  • 副作用(サイドエフェクト)とは何か
  • ローカルストレージへの保存・読み込み
TODOアプリ(第7回:完成イメージ)その①
TODOアプリ(第7回:完成イメージ)その①

TODOアプリ開発シリーズのまとめ記事はこちら



ケケンタ

ケケンタのITブログでは、WebアプリPHPLaravel)やWeb制作WordPressコーディング)について情報を発信しています。
学習中の方や実務をされている方など多くの方にアクセスいただいていますので、ぜひほかの記事も参考にしてみてください!


運動不足、気になっていませんか?

もしプログラミング学習やお仕事で運動不足が気になっているなら

連続屈伸運動がおすすめです!

ボタンにカーソルを合わせるだけ
カウントダウンが始まるタイマーをご用意してみました!

ケケンタ

無理のない範囲で、ぜひ隙間時間に屈伸運動を取り入れてみて下さい!

タイマースタート

3:00

※運動不足だと連続3分で取り組んでもかなり息が切れます
(僕は加えて気分もちょっと悪くなりました……)
絶対にご無理の無い範囲でお取り組みください!



目次

前回までのコード

第6回までで作成・修正したファイル構成と主要なコードは以下の通りです。

ファイル構成

todo-app/
├── src/
│   ├── App.js
│   ├── App.css
│   ├── TodoItem.js
│   ├── TodoList.js
│   ├── TodoHeader.js
│   ├── TodoForm.js
│   └── TodoFilter.js

TodoItem.js

import React, { useState } from 'react';

function TodoItem({ id, text, completed, priority = "medium", onToggle, onDelete, onEdit }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(text);

  const handleEdit = () => {
    setIsEditing(true);
    setEditText(text);
  };

  const handleSave = () => {
    if (editText.trim()) {
      onEdit(id, editText);
      setIsEditing(false);
    }
  };

  const handleCancel = () => {
    setIsEditing(false);
    setEditText(text);
  };

  const handleKeyDown = (event) => {
    if (event.key === 'Enter') {
      handleSave();
    }
    if (event.key === 'Escape') {
      handleCancel();
    }
  };

  return (
    <div className={`todo-item priority-${priority}`}>
      <input
        type="checkbox"
        checked={completed}
        onChange={() => onToggle(id)}
      />
      {isEditing ? (
        <div className="edit-section">
          <input
            type="text"
            value={editText}
            onChange={(e) => setEditText(e.target.value)}
            onKeyDown={handleKeyDown}
            className="edit-input"
            autoFocus
          />
          <button onClick={handleSave} className="save-button">保存</button>
          <button onClick={handleCancel} className="cancel-button">キャンセル</button>
        </div>
      ) : (
        <div className="todo-content">
          <span style={{ textDecoration: completed ? 'line-through' : 'none' }}>{text}</span>
          <span className={`priority-badge ${priority}`}>{priority === 'high' ? '高' : priority === 'medium' ? '中' : '低'}</span>
          <div className="todo-actions">
            <button onClick={handleEdit} className="edit-button">編集</button>
            <button onClick={() => onDelete(id)} className="delete-button">削除</button>
          </div>
        </div>
      )}
    </div>
  );
}

export default TodoItem;

TodoList.js

import React from 'react';
import TodoItem from './TodoItem';

function TodoList({ todos, onToggle, onDelete, onEdit }) {
  if (todos.length === 0) {
    return (
      <div className="empty-state">
        <p>TODOがありません。新しいTODOを追加してください。</p>
      </div>
    );
  }
  return (
    <div className="todo-list">
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          id={todo.id}
          text={todo.text}
          completed={todo.completed}
          priority={todo.priority}
          onToggle={onToggle}
          onDelete={onDelete}
          onEdit={onEdit}
        />
      ))}
    </div>
  );
}

export default TodoList;

App.js

import React, { useState } from 'react';
import './App.css';
import TodoHeader from './TodoHeader';
import TodoList from './TodoList';
import TodoForm from './TodoForm';
import TodoFilter from './TodoFilter';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Reactを学ぶ", completed: false, priority: "high" },
    { id: 2, text: "TODOアプリを作る", completed: true, priority: "medium" },
    { id: 3, text: "データベース連携", completed: false, priority: "low" },
    { id: 4, text: "デプロイする", completed: false, priority: "medium" }
  ]);
  const [filter, setFilter] = useState('all');

  const filteredTodos = todos.filter(todo => {
    if (filter === 'all') return true;
    if (filter === 'completed') return todo.completed;
    if (filter === 'pending') return !todo.completed;
    return true;
  });

  const completedCount = todos.filter(todo => todo.completed).length;

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

  const addTodo = (newTodo) => {
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

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

  const editTodo = (id, newText) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id
          ? { ...todo, text: newText }
          : todo
      )
    );
  };

  return (
    <div className="App">
      <TodoHeader
        title="React TODOアプリ"
        totalCount={todos.length}
        completedCount={completedCount}
      />
      <TodoForm onAddTodo={addTodo} />
      <TodoFilter
        filter={filter}
        onFilterChange={setFilter}
        totalCount={todos.length}
        activeCount={todos.filter(todo => !todo.completed).length}
        completedCount={completedCount}
      />
      <TodoList
        todos={filteredTodos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
        onEdit={editTodo}
      />
    </div>
  );
}

export default App;

App.css

.edit-section {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
}
.edit-input {
  flex: 1;
  padding: 8px 12px;
  border: 2px solid #007bff;
  border-radius: 4px;
  font-size: 14px;
  outline: none;
}
.save-button {
  padding: 6px 12px;
  background-color: #28a745;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.save-button:hover {
  background-color: #218838;
}
.cancel-button {
  padding: 6px 12px;
  background-color: #6c757d;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.cancel-button:hover {
  background-color: #5a6268;
}
.todo-actions {
  display: flex;
  gap: 8px;
  margin-left: auto;
}
.edit-button {
  padding: 4px 8px;
  background-color: #ffc107;
  color: #212529;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.edit-button:hover {
  background-color: #e0a800;
}
.delete-button {
  padding: 4px 8px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.delete-button:hover {
  background-color: #c82333;
}

useEffectと副作用の基礎

編集・削除機能を実装する前に、必要な前提知識を整理しておきましょう。

副作用(サイドエフェクト)とは

レンダリング以外の処理(データ取得、ローカルストレージ操作、タイマー設定など)のことを指します。

useEffectの基本構文

import { useEffect } from 'react';

useEffect(() => {
  // 副作用処理
  return () => {
    // クリーンアップ処理(省略可)
  };
}, [依存配列]);
  • 依存配列が空:マウント時のみ実行
  • 依存配列に値:その値が変化したときに実行
マウントとは?

Reactにおけるマウントとは、コンポーネントが画面上に初めて表示されるタイミングのことを指します。Reactでは、コンポーネントが生成されてDOMに追加されることを「マウント」と呼びます。

【サンプル】useEffectでコンソールにメッセージを表示

import React, { useEffect } from 'react';

function SampleComponent() {
  useEffect(() => {
    console.log('コンポーネントがマウントされました');
    return () => {
      console.log('コンポーネントがアンマウントされました');
    };
  }, []);

  return <div>useEffectのサンプル</div>;
}

ローカルストレージとは?

ローカルストレージは、Webブラウザにデータを保存できる仕組みです。保存したデータはページをリロードしたり、ブラウザを閉じたりしても消えずに残ります。これにより、ユーザーが入力した情報やアプリの状態を永続的に保持することができます。

例えば、TODOリストの内容をローカルストレージに保存しておけば、次回アクセス時にも前回のリストがそのまま表示されるようになります。

保存先と確認方法

  • ローカルストレージの保存先は、各ブラウザのlocalStorage領域です。
  • Chromeの場合、F12キーで開発者ツールを開き、「Application」タブ → 「Storage」→「Local Storage」→該当ドメインを選択すると、保存されているデータを確認・編集できます。
  • 保存キー(例:todos)や値(JSON形式)もここで直接確認できます。
  • ローカルストレージには保存容量(一般的に5MB程度)の制限があります。
  • 同じドメイン内のページであればデータは共有されますが、他のブラウザや端末とは共有されません。
  • セキュリティ上、パスワードや個人情報などの機密データは保存しないようにしましょう。
  • ユーザーが手動でローカルストレージを削除することも可能です。

【実践】ローカルストレージへのTODO保存・復元

ここからは、実際に手を動かして、TODOリストをローカルストレージに保存・復元する機能を追加していきましょう。

App.jsの修正

import React, { useState, useEffect } from 'react';
// ...既存のimport

function App() {
  const [todos, setTodos] = useState(() => {
    // 初回マウント時にローカルストレージから読み込み
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });

  // ...他のstateや関数

  // todosが変化するたびにローカルストレージへ保存
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  // ...JSXはこれまで通り
}

動作確認

実装が完了したら、以下の点を確認してください。

TODOアプリ(第7回:完成イメージ)その①
TODOアプリ(第7回:完成イメージ)その①
TODOアプリ(第7回:完成イメージ)その①
TODOアプリ(第7回:完成イメージ)その①
  • TODOを追加・編集・削除しても、ページをリロードしても内容が保持される
  • ローカルストレージに「todos」というキーでデータが保存されている

FAQ

ローカルストレージに保存されない場合

localStorageのキー名やJSONの扱い、useEffectの依存配列が正しいか確認してください。

useEffectが何度も実行される場合

依存配列の指定が正しいか確認してください。

まとめ

今回は、useEffectフックを使った副作用処理と、ローカルストレージへのTODO保存・復元を実装しました。

  • useEffectの基本的な使い方
  • 副作用の概念
  • ローカルストレージとの連携
  • クリーンアップ処理の実装
ケケンタ

これでついに「ReactのTODOアプリ開発シリーズの基本編」は最後の記事を迎えました!
ここまで本当にお疲れ様でした!

ここまでの内容で、TODOアプリに必要な最低限の機能は実装することができました。

さらなるスキルアップを目指す方は、このTODOアプリを自分なりにカスタマイズしていくとよりReactへの理解を深めることができるかと思います。

なお、本シリーズは次回から「発展編」へ突入します。「基本編」で作成したTODOアプリをベースに、API連携やデータベース連携など、さらなる発展的な技術を学習していきます。

「発展編 第1回」となる次回の記事では、CSS-in-JSを活用したスタイリングとUI/UX改善に挑戦していきます。

次回もどうぞお楽しみに!

次回「第8回(発展編 第1回)」の記事は鋭意制作中です。


僕が実際にReact学習に使用した書籍です

【React】useEffectと副作用・ローカルストレージ|TODOアプリ【基本編:第7回】のアイキャッチ画像

この記事が気に入ったら
フォローしてね!

この記事が良いと思ったらシェアしてね!

コメント

コメントする

CAPTCHA


目次