Django+Reactで学ぶプログラミング基礎(37): Reduxチュートリアル(UIレイヤーのReactと統合)

##### [前回] [Django+Reactで学ぶプログラミング基礎(36): Reduxチュートリアル(MiddlewareでStoreをカスタマイズ)](https://qiita.com/mingchun_zhao/items/98c027531849c1114411)

## はじめに

前回は、Middlewareを用いてStoreをカスタマイズしました。
今回は、RuduxとUIレイヤーの統合です。

## 今回の内容

[Reduxの基礎、パート5：UIとReact](https://redux.js.org/tutorials/fundamentals/part-5-ui-react)

- ReduxとUIレイヤー
- ReactでReduxを使用

## ReduxとUIレイヤー

- ReduxはスタンドアロンのJSライブラリ
    - クライアントとサーバーの両方で使用可能
    - 任意のUIフレームワークで使用可能で、すべてのUIで同じように機能
        - React、Vue、Angular、Ember、jQuery、vanilla JavaScriptで使用可能
        - 特にReactと連携するように設計され、UIをstateの関数として記述できる

- ReduxとUIレイヤーの統合手順
    - Redux storeを作成
    - state更新をsubscribe
    - subscriptionコールバックの処理
        - 現在storeのstateを取得
        - UIに必要なデータを抽出
        - データでUIを更新
    - 必要に応じ、UIを初期stateでレンダリング
    - Redux actionをdispatchし、UI入力に応答

- 上記統合手順を、カウンターアプリの例から
```
// 1) Redux storeを作成
const store = Redux.createStore(counterReducer)

// 2) state更新をSubscribeし、データ更新時に再レンダリング可能に
store.subscribe(render)

// UIはtextのみのHTML要素
const valueEl = document.getElementById('value')

// 3) subscription callback内の処理:
function render() {
  // 3.1) 現在storeのstateを取得
  const state = store.getState()
  // 3.2) 必要データを抽出
  const newValue = state.value.toString()
  // 3.3) 新しい値でUIを更新
  valueEl.innerHTML = newValue
}

// 4) UIをstoreの初期stateでレンダリング
render()

// 5) UI入力に応じ、actionをdispatch
document.getElementById('increment').addEventListener('click', function () {
  store.dispatch({ type: 'counter/incremented' })
})
```

## ReactでReduxを使用

- `React-Redux`UIバインディングライブラリを使用
    - インストール: `npm install react-redux`

### ビジネス要件に基づき、コンポーネントツリーを設計

- `<App>`: アプリ全般をレンダリングするルートコンポーネント
    - `<Header>`: `新しいtodo`入力と`すべてのtodoを完了`するチェックボックス
    - `<TodoList>`: フィルタリング結果に基づき表示されるToDoリスト
        - `<TodoListItem>`: 単一ToDoリストアイテム
            - 完了ステータスを切り替えるチェックボックスとカラーカテゴリセレクターを持つ
    - `<Footer>`: アクティブなタスクの数を表示
        - 完了ステータスと色のカテゴリを用いてリストをフィルタリングするコントロールも表示

- アプリのReact UI(Redux関連ロジックを追加する前)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/aad75a7e-1738-5cf1-fe91-e61951ef111f.png)

### useSelectorフックを使用し、storeからstateを取得

- ToDoアイテムリストを表示するため
    - storeからtodoリストを読み取る
    - todoアイテムごとに`<TodoListItem>`コンポーネントを表示する`<TodoList>`コンポーネントを作成

- useSelectorフック
    - React-Reduxフックで、ReactコンポーネントとRedux storeとの通信機能を提供
        - stateを取得しactionをdispatch
    - selector関数を引数として受け取る
    - selectorは、R​​edux Storeのstateから、値を読み取り、結果を返す関数
        - selectorの例1: todoアイテム配列のstate.todosを返す
            - `const selectTodos = state => state.todos`
        - selectorの例2: 完了マークされたtodoの数を返す
```
const selectTotalCompletedTodos = state => {
  const completedTodos = state.todos.filter(todo => todo.completed)
  return completedTodos.length
}
```

- ToDoの配列を`<TodoList>`コンポーネントに読み込む
    - まず、react-reduxライブラリからuseSelectorフックをインポート
    - 次に、selector関数を引数として呼び出す
```src/features/todos/TodoList.js
import React from 'react'
import { useSelector } from 'react-redux'
import TodoListItem from './TodoListItem'

const selectTodos = state => state.todos

const TodoList = () => {
  const todos = useSelector(selectTodos)

  // since `todos` is an array, we can loop over it
  const renderedListItems = todos.map(todo => {
    return <TodoListItem key={todo.id} todo={todo} />
  })

  return <ul className="todo-list">{renderedListItems}</ul>
}

export default TodoList
```

- コード説明
    - Reactの`<TodoList>`コンポーネントが初めてレンダリングされるとき
        - useSelectorフックはselectTodosを呼び出し、Redux stateを渡す
        - selectorの返却値はフックによって`<TodoList>`コンポーネントに渡される
            - コンポーネントの`const todos`に、Redux storeのstate内のstate.todos配列が代入される
    - `{type：'todos/todoAdded'}`actionがdispatchされたら
        - useSelectorは自動的にRedux Storeにsubscribeし、storeの変更をlisten
        - Reduxのstateがreducerによって更新されると、Reactの`<TodoList>`コンポーネントは、新しいToDoのリストを検知し再レンダリング

- ※注意: selector関数でstate.todos配列への新しい参照を返却してはいけない
    - useSelectorは、厳密な`===`参照比較を使用し結果を比較
    - selectorが新しい参照を作成して返却すると
        - stateが変更しなくても、actionがdispatchされる度にコンポーネントが再レンダリングされる
```
// Bad: array.map()は常に新しい配列参照を返す
const selectTodoDescriptions = state => {
  // This creates a new array reference!
  return state.todos.map(todo => todo.text)
}
```

- ※ selector関数を個別変数として宣言する必要なし
```
const todos = useSelector（state => state.todos）
```

### useDispatchを使用しactionをdispatch

- Reactのコンポーネントから、Redux storeにアクションをdispatchする方法
    - Reactコンポーネントからstoreにアクセスできないため、`store.dispatch(action)`をコールできない
    - React-Redux useDispatchフックを使用し、storeのdispatchを実現
        - `const dispatch = useDispatch()`を定義し、必要に応じて`dispatch(someAction)`を呼び出す

- `<Header>`コンポーネントで、新しいtodoアイテムを作成する`{type：'todos/todoAdded'`actionをdispatch
    - まず、React formコンポーネントを作成し、フォームから`制御された入力`を可能に
    - 次に、ユーザーがEnterキーを押すと、actionがdispatchされる

```src/features/header/Header.js
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'

const Header = () => {
  const [text, setText] = useState('')
  const dispatch = useDispatch()

  const handleChange = e => setText(e.target.value)

  const handleKeyDown = e => {
    const trimmedText = e.target.value.trim()
    // If the user pressed the Enter key:
    if (e.key === 'Enter' && trimmedText) {
      // Dispatch the "todo added" action with this text
      dispatch({ type: 'todos/todoAdded', payload: trimmedText })
      // And clear out the text input
      setText('')
    }
  }

  return (
    <input
      type="text"
      placeholder="What needs to be done?"
      autoFocus={true}
      value={text}
      onChange={handleChange}
      onKeyDown={handleKeyDown}
    />
  )
}

export default Header
```

### Reactコンポーネントで使用するstoreをReact-Reduxに指示

- React-ReduxフックはJS関数で、store.jsからstoreを自動的にインポートできない
    - コンポーネントが通信を行うRedux storeストアを指示する必要あり
    - `<App>`全体で`<Provider>`コンポーネントをレンダリングし、Redux storeをpropとして`<Provider>`に渡す
        - アプリのすべてのコンポーネントがRedux storeにアクセスできるようになる

- メインのindex.jsファイルに追加
```src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'

import App from './App'
import store from './store'

ReactDOM.render(
  // Render a `<Provider>` around the entire `<App>`,
  // and pass the Redux store to as a prop
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)
```

- コード説明
    - React-ReduxをReactで使用する際の重要な部分が含まれている
        - useSelectorフックを呼び出し、Reactコンポーネントのデータを読み取る
        - useDispatchフックを呼び出し、Reactコンポーネントのactionをdispatch
        - `<App>`コンポーネント全体に`<Providerstore={store}>`を配置し、他のコンポーネントがstoreと通信できるように

### アプリを起動しUIを確認

- VS Codeのターミナルで、アプリを起動すると、ブラウザが開かれる

```
C:\kanban\todo>cd redux-fundamentals-example-app
C:\kanban\todo\redux-fundamentals-example-app>npm start
```

- ブラウザで、アプリを操作してみる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/1b07bb5a-eb1a-8e7f-8b50-accc418ffb0a.png)

## おわりに

ReduxとUIレイヤーのReactとの連携を勉強しました。
次回も続きます。お楽しみに。