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で学ぶプログラミング基礎(29): Reduxチュートリアル(構成要素とデータフローの概要)

Last updated at Posted at 2022-06-18
[前回] Django+Reactで学ぶプログラミング基礎(28): Reduxチュートリアル(準備と概要)

はじめに

前回は、チュートリアルの概要と、なぜReduxが必要か、
を勉強しました。
今回は、Reduxアプリの構成要素と、データフローの概要を、
サンプルを通して理解します。

今回の内容

Reduxの基本

  • Reduxアプリの構成要素
  • Reduxアプリのデータフロー
  • Reduxコアをサンプルアプリで理解

Reduxアプリの構成要素

image.png

※ 引用元: Reduxアプリを介したデータフロー

  • Store

    • すべてのReduxアプリの中心
    • アプリのグローバル状態を保持するコンテナ
    • いくつかの特別な機能を備えたJavaScriptオブジェクト
  • State/View(UI)/Reducer/Actions

    • State
      • アプリの状態を保持する唯一の手段
      • 特定時点のアプリ状態を表す
    • View
      • 現在の状態に基づく、UIの宣言型説明
      • UIは状態に基づきレンダリングされる
    • Reducer
      • storeで、reducer関数を実行し、新しいstateを計算
    • Actions
      • アプリで、ユーザー入力による、発生イベントおよび状態更新のトリガー
      • ユーザーがボタンをクリックするなどアクションを起こすと
        • stateが、発生内容に基づき更新される
        • UI(view)は、新しい状態を読み取り、再レンダリングされる
        • actionが、storeにdispatchされる
        • storeが、reducer関数で新しいstateを計算

Reduxアプリのデータフロー

  • UIをクリックし、イベント発生
    image.png

  • ActionがStoreにDispatchされる
    image.png

  • ReducerがStateを計算し、その結果によりUIが更新される
    image.png

※ 引用元: Reduxアプリを介したデータフロー

  • アプリでアクションが発生したら

    • 発生イベントを表すプレーン(plain)なactionオブジェクトを作成
      • actionをstoreにdispatchし、何が発生したか通知
        • storeはroot reducer関数を実行し、古いstateとactionに基づき、新しいstateを計算
          • storeはsubscriberにstateが更新されたことを通知
            • view(UI)が、subscriberにより新しいデータで更新される
  • 上述フローが、storeに保持されているstateを変更する唯一の方法

    • ※ storeに保持されたstateを直接変更してはいけない

Reduxコアをサンプルアプリで理解

環境準備

アプリコード作成

  • srcフォルダーに新規ファイルindex.htmlを追加
src/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Redux basic example</title>
    <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
  </head>
  <body>
    <div>
      <p>
        Clicked: <span id="value">0</span> times
        <button id="increment">+</button>
        <button id="decrement">-</button>
        <button id="incrementIfOdd">Increment if odd</button>
        <button id="incrementAsync">Increment async</button>
      </p>
    </div>
    <script>
      // Define an initial state value for the app
      const initialState = {
        value: 0
      };

      // Create a "reducer" function that determines what the new state
      // should be when something happens in the app
      function counterReducer(state = initialState, action) {
        // Reducers usually look at the type of action that happened
        // to decide how to update the state
        switch (action.type) {
          case "counter/incremented":
            return { ...state, value: state.value + 1 };
          case "counter/decremented":
            return { ...state, value: state.value - 1 };
          default:
            // If the reducer doesn't care about this action type,
            // return the existing state unchanged
            return state;
        }
      }

      // Create a new Redux store with the `createStore` function,
      // and use the `counterReducer` for the update logic
      const store = Redux.createStore(counterReducer);

      // Our "user interface" is some text in a single HTML element
      const valueEl = document.getElementById("value");

      // Whenever the store state changes, update the UI by
      // reading the latest store state and showing new data
      function render() {
        const state = store.getState();
        valueEl.innerHTML = state.value.toString();
      }

      // Update the UI with the initial data
      render();
      // And subscribe to redraw whenever the data changes in the future
      store.subscribe(render);

      // Handle user inputs by "dispatching" action objects,
      // which should describe "what happened" in the app
      document
        .getElementById("increment")
        .addEventListener("click", function () {
          store.dispatch({ type: "counter/incremented" });
        });

      document
        .getElementById("decrement")
        .addEventListener("click", function () {
          store.dispatch({ type: "counter/decremented" });
        });

      document
        .getElementById("incrementIfOdd")
        .addEventListener("click", function () {
          // We can write logic to decide what to do based on the state
          if (store.getState().value % 2 !== 0) {
            store.dispatch({ type: "counter/incremented" });
          }
        });

      document
        .getElementById("incrementAsync")
        .addEventListener("click", function () {
          // We can also write async logic that interacts with the store
          setTimeout(function () {
            store.dispatch({ type: "counter/incremented" });
          }, 1000);
        });
    </script>
  </body>
</html>

image.png

ブラウザで、カウンターアプリを使ってみる

  • F5を押してアプリを実行

    • デバッガーとしてWeb App(Chrome)を選択
      image.png
  • ブラウザで、以下ボタンでさまざまなカウント操作を行ってみる

    • +をクリック: 1増やす
    • -をクリック: 1減らす
    • Increment if oddをクリック: 奇数時のみ増やす
    • Increment asyncをクリック: 1秒遅延と伴う非同期加算

コード説明

State/Actions/Reducers

  • まず、カウンターの状態を表すstateの初期値を定義
    • Reduxアプリでstateの構成
      • stateのroot部分として、JSオブジェクトを持つ
        • サンプルでは、initialState
        • そのJSオブジェクト内に他の値が含まれる
src/index.html
// Define an initial state value for the app
const initialState = {
  value: 0
}
  • 次に、reducer関数を定義
    • reducerは、二つの引数を受け取る
      • state: 現在の状態を表す
      • action: 何が起こったかを示す
    • Reduxアプリ起動時、まだstateがないため
      • reducerのデフォルト値としてinitialStateを指定
    • actionオブジェクトのtypeフィールド
      • actionの一意の名前を表すため、意味が分かりやすい名前に
        • 例えば、カウンターを増やすactionのtype
          • counter/incrementedと記述
      • actionのtypeに基づいて、返却されるstateが異なる
        • stateが変更された場合、新しいstateオブジェクトを返す
        • stateが変更されていない場合、既存のstateオブジェクトを返す
          • ※ stateは、イミュータブルに更新
            • 元のstateオブジェクトを直接変更せず、既存のstateをコピーしそれを更新
src/index.html
// Create a "reducer" function that determines what the new state
// should be when something happens in the app
function counterReducer(state = initialState, action) {
  // Reducers usually look at the type of action that happened
  // to decide how to update the state
  switch (action.type) {
    case 'counter/incremented':
      return { ...state, value: state.value + 1 }
    case 'counter/decremented':
      return { ...state, value: state.value - 1 }
    default:
      // If the reducer doesn't care about this action type,
      // return the existing state unchanged
      return state
  }
}

store

  • ReduxライブラリのcreateStoreAPIを呼び出し、storeインスタンスを作成
    • reducer関数をcreateStoreに渡す
    • createStoreはreducer関数を使用し
      • 初期値を持つstateを生成
      • 将来更新時、stateを計算
src/index.html
// Create a new Redux store with the `createStore` function,
// and use the `counterReducer` for the update logic
const store = Redux.createStore(counterReducer)

view(UI)

  • UIは、画面に既存のstateを表示
    • ユーザーがアクションを起こすと
      • アプリはそのデータを更新してから、新しい値でUIを再レンダリング
    • store.getState()メソッドを使用し、storeから最新stateを取得
      • その値を用いてUIを更新/表示
    • store.subscribe()メソッドを呼び出し、storeが更新されるたびに呼び出されるsubscriberコールバック関数を渡す
      • サンプルでは、render関数をsubscriberとして渡している
        • storeが更新されるたびに、UIを最新の値で更新可能
    • Redux自体は、どこでも使用できるスタンドアロンのライブラリ
      • 任意のUIレイヤーでReduxを使用可能
src/index.html
// Our "user interface" is some text in a single HTML element
const valueEl = document.getElementById('value')

// Whenever the store state changes, update the UI by
// reading the latest store state and showing new data
function render() {
  const state = store.getState()
  valueEl.innerHTML = state.value.toString()
}

// Update the UI with the initial data
render()
// And subscribe to redraw whenever the data changes in the future
store.subscribe(render)

actionをdispatch

  • actionオブジェクトを作成し、storeにdispatchすることで、ユーザー入力に応答
    • store.dispatch(action)を呼び出すと
      • storeはreducerを実行し、更新後のstateを計算
      • subscriberを実行し、UIを更新
    • サンプルでは、以下のような複数パターンでactionをdispatchし、reducerに計算させる
      • 現在のカウンター値から加算または減算
      • カウンター値が奇数の場合のみ加算
      • 1秒遅延後に非同期に加算
src/index.html
// Handle user inputs by "dispatching" action objects,
// which should describe "what happened" in the app
document.getElementById('increment').addEventListener('click', function () {
  store.dispatch({ type: 'counter/incremented' })
})

document.getElementById('decrement').addEventListener('click', function () {
  store.dispatch({ type: 'counter/decremented' })
})

document
  .getElementById('incrementIfOdd')
  .addEventListener('click', function () {
    // We can write logic to decide what to do based on the state
    if (store.getState().value % 2 !== 0) {
      store.dispatch({ type: 'counter/incremented' })
    }
  })

document
  .getElementById('incrementAsync')
  .addEventListener('click', function () {
    // We can also write async logic that interacts with the store
    setTimeout(function () {
      store.dispatch({ type: 'counter/incremented' })
    }, 1000)
  })

おわりに

Reduxアプリの構成要素とデータフローを、
サンプルを通じて勉強しました。
次回も続きます。お楽しみに。

[次回] Django+Reactで学ぶプログラミング基礎(30): Reduxチュートリアル(概念とデータフローの詳細)
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?