3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Django+Reactで学ぶプログラミング基礎(32): Reduxチュートリアル(Todoアプリの設計と実装)

Last updated at Posted at 2022-06-20
[前回] Django+Reactで学ぶプログラミング基礎(31): Reduxチュートリアル(Todoアプリの要件定義と設計)

はじめに

前回は、Todoアプリの要件定義と設計を行いました。
今回は、設計の残り部分および実装に関してです。

今回の内容

Todoアプリの構築

  • Actionの設計
  • Reducerの実装

Actionの設計

  • actionは、typeフィールドを持つプレーンなJavaScriptオブジェクト

    • アプリで何が発生したかを表すイベント
    • 発生イベントに関する必要最小限の情報のみ含める
  • Todoアプリで対応すべき、actionリスト

    • ユーザー入力のテキストに基づき、新しいTodoエントリを追加
    • Todoを完了ステータスに切り替える
    • Todoのカラーカテゴリを選択
    • Todoを削除
    • すべてのTodoに完了マークを付ける
    • 完了したタスクをすべて削除
    • 完了ステータスによるフィルタリング
    • 新しいカラーフィルターを追加
    • 既存のカラーフィルターを取り外す
  • actionのtypeフィールドは、actionの一意識別子として使用される

    • actionを分かりやすく説明できる文字列を、typeフィールドにセット
  • actionのpayloadフィールドは、acitonの説明に必要な追加データを保存

    • 数値/文字列/オブジェクト(内部に複数フィールドを持つ)などのデータ型を使用可
  • Todoアプリで発生可能イベントに基づき、actionリストを作成

    • カラーフィルターの操作は、追加用と削除済み用の2種類存在
      • 2つのaction.typeに分割してもよいが
      • ここでは、payloadフィールドのchangeTypeを用いて区別
{type: 'todos/todoAdded', payload: todoText}
{type: 'todos/todoToggled', payload: todoId}
{type: 'todos/colorSelected, payload: {todoId, color}}
{type: 'todos/todoDeleted', payload: todoId}
{type: 'todos/allCompleted'}
{type: 'todos/completedCleared'}
{type: 'filters/statusFilterChanged', payload: filterValue}
{type: 'filters/colorFilterChanged', payload: {color, changeType}}

Reducerの実装

  • Reducerは、現在のstateとactionを引数と取り、新しいstateの結果を返す関数
    • (state、action) => newState

ルートReducerの作成

  • Reduxアプリには、1つのReducer関数ルートReducer関数しか存在しない
    • store作成のcreateStoreメソッドに、引数としてルートReducer関数が渡される
    • ルートReducer関数は、dispatchされたすべてのactionを処理し、新しいstateの結果を計算する役割を果たす

まず、srcフォルダーにreducer.jsファイルを作成

  • すべてのreducerには初期状態が必要
    • 初期値としてTodoエントリを三つ追加
  • 次に、reducer関数内にロジック概要を記述
src/reducer.js
const initialState = {
  todos: [
    { id: 0, text: 'Learn React', completed: true },
    { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
    { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
  ],
  filters: {
    status: 'All',
    colors: []
  }
}

// initialStateをデフォルト値として使用
export default function appReducer(state = initialState, action) {
  // reducerは通常actionのtypeフィールドの値により、何が起こったか判断
  switch (action.type) {
    // actionのtype別処理を行う
    default:
      // If this reducer doesn't recognize the action type, or doesn't
      // care about this specific action, return the existing state unchanged
      return state
  }
}

image.png

  • reducerは、アプリ初期化時にstate値がundefinedで呼び出される場合あり
    • 対処として、stateの初期値を指定し、残りのreducerコードが機能するように
    • reducerは通常、ES6のデフォルトの引数構文を使用し、stateの初期値を提供
      • (state = initialState、action)

次に、todos/todoAddedactionを処理するロジックを追加

  • まず、カレントactionのtypeが特定文字列と一致するか確認
  • 次に、変更されなかったフィールドについても、すべてのstateを含む新しいオブジェクトを返す必要あり
    • reduce()メソッドは、配列の要素を一つずつ取り出し、指定した処理を行っていき、最終的に一つの値を返す関数
      • todos.reduce()は、初期値が-1のmaxId変数を、todos配列の要素と一つずつ比較し、最終的に配列要素の最大値を返す
src/reducer.js
function nextTodoId(todos) {
  const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
  return maxId + 1
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
  // The reducer normally looks at the action type field to decide what happens
  switch (action.type) {
    // Do something here based on the different types of actions
    case 'todos/todoAdded': {
      // We need to return a new state object
      return {
        // that has all the existing state data
        ...state,
        // but has a new array for the `todos` field
        todos: [
          // with all of the old todos
          ...state.todos,
          // and the new todo object
          {
            // Use an auto-incrementing numeric ID for this example
            id: nextTodoId(state.todos),
            text: action.payload,
            completed: false
          }
        ]
      }
    }
    default:
      // If this reducer doesn't recognize the action type, or doesn't
      // care about this specific action, return the existing state unchanged
      return state
  }
}

Reducerのルール

  • 既存stateの直接変更は許可されない

    • 代わりに、既存のstateをコピーし、コピーされた値に変更を加えることで、イミュータブルな更新を行う
    • 非同期ロジックやその他副作用を伴う処理を実行してはいけない
      • 副作用とは、stateを変更する、またはstate値の返却以外の下記のような処理を指す
        • コンソールに値を記録
        • ファイル保存
        • 非同期タイマーの設定
        • AJAX HTTPリクエストを作成
        • 関数外部に存在するstateを変更、または関数の引数を書き換える
        • 乱数または一意のランダムID(Math.random()Date.now()など)の生成
  • 上記ルールが必要な理由

    • Reduxの目標の1つは、コードを予測可能にすること
      • 関数の出力が入力引数からのみ計算されると、そのコードがどのように機能するか理解し、テストも簡単になる
    • 関数が外部の変数に依存する、またはランダムに動作する場合
      • 関数を実行したときに何が起こるか予測不能
    • 関数が引数や他の値を変更した場合
      • アプリが予期しない動作をする可能性あり
        • stateを更新したが、UIが更新されないなどバグの元となる
    • Redux DevToolsの機能の一部は、reducerがこれらのルールに正しく従うことを前提とする

Reducerとイミュータブルな更新

  • Reduxでは、reducerが元のstate値を直接変更することを許可しない
    • 許可されないstate変更例
      • state.value = 123
  • Reduxでstateを直接変更してはならない理由
    • UIが正しく更新されない、最新の値が表示されない、などバグ発生の可能性あり
    • stateが更新された理由と方法の追跡が難しくなる
    • テスト作成が難しくなる
    • タイムトラベルデバッグが正しく機能しなくなる
  • 元のstateを変更せず、更新されたstateを返却する方法
    • reducerは、元のstateのコピーを作成し、そのコピーを変更
      • スプレッド演算子を用いて、安全にstateを変更する例
        • return { ...state, value: 123}
    • データがネストされている場合、イミュータブルな更新を行う方法
      • 更新が必要なネストレベルに対しても、データコピーを作成する必要あり
      • 実際のアプリでは、Redux Toolkitを使用し、複雑なネストに対しイミュータブルな更新を実施(手動でコピー作成する必要なし)

おわりに

Reduxを用いた、Todoアプリの設計と実装を行いました。
次回も続きます。お楽しみに。

[次回] Django+Reactで学ぶプログラミング基礎(33): Reduxチュートリアル(Todoアプリの実装(続き))
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?