【React】Redux Toolkit のチュートリアルのサンプルコードを読んでみる

# はじめに
React を習得するまでの軌跡をメモっていく備忘録的な記事です。

## Redux Toolkit を使ってみる

2019年にリリースされた React の状態管理のライブラリで従来の Redux をより使いやすくしたものらしい。

> Redux Toolkit の[公式サイト](https://redux-toolkit.js.org/tutorials/intermediate-tutorial)

> 公式サイトの Intermediate Tutorial を参考にしました。
サンプルコードのGitHubのリポジトリは[こちら](https://github.com/reduxjs/rtk-convert-todos-example)

サンプルコードの中身は todo アプリで
仕様としては

-  一個一個の todo は text(すべきこと) と completed(完了したかどうか) を持つ
- フッターのボタンで all(全てのTODOを表示), active(完了してないTODOを表示) , completed(完了したTODOを表示) を切り替えることができる

UIは以下のような感じです。


 ![スクリーンショット 2020-12-06 19.55.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559794/896763a9-60d3-2b74-25f2-0d30cc8f7196.png)


## 実際にソースコードを読む

ファイル構成はこんな感じです。

![スクリーンショット 2020-12-06 23.59.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559794/b1df6ede-4f59-f9d0-e7e7-b2be3acc3603.png)

Redux Toolkitの要になりそうな部分だけ取り上げています。

ソースコード中にコメントを入れる形で書いていきます。

~~~jsx:index.js
import React from 'react'
import { render } from 'react-dom'
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'


const store = configureStore({
  // store を定義する
  // rootReducer は createSliceで作った Reducer 達を combineReducerにより合体させたもの
  // (↑ reducers/index.js で定義している)
  reducer: rootReducer
})

render(
  // 子コンポーネントが store に接続できるように
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

~~~

### TODOを追加したり、完了状態を変更する部分の実装

~~~features/todos/todoSlice.js
import { createSlice } from '@reduxjs/toolkit' // createSlice を使えるようにする

// slice を作成する Redux Toolkitのメイン部分
// State, Reducer, Action を一気に生成する
const todosSlice = createSlice({
  name: 'todos', // slice の名前を設定
  initialState: [], // state の初期値を設定
  reducers: { // reducer を設定 複数の reducer を設定できる
     // prepare のコールバックを用いた書き方
    addTodo: {
      reducer(state, action) {
        const { id, text } = action.payload // action.payload には reducer に渡したい正味の値が入る
        state.push({ id, text, completed: false }) // state に新規の値を追加している
      },
      //prepare のコールバックを使うとstateを更新する前に色々と事前に編集できて便利
      prepare(text) { 
        return { payload: { text, id: nextTodoId++ } }
      }
    },
    // こっちが通常の書き方
    toggleTodo(state, action) {
      // state の中から id に一致した todo を見つける
      const todo = state.find(todo => todo.id === action.payload)
      if (todo) {
        todo.completed = !todo.completed // 見つけた todo の　complete　を更新する
      }
    }
  }
})

// addTodo と toggleTodo の Action を生成できる関数を export
export const { addTodo, toggleTodo } = todosSlice.actions

// Reducer を export
export default todosSlice.reducer
~~~


connect, mapDispatch, mapStateToProps についてはこちらの記事が大変参考になりました。
[mapStateToPropsとmapDispatchToPropsの理解の仕方](https://ocws.jp/blog/post1866/#mapstatetopropsmapdispatchtoprops)

~~~jsx:features/todos/AddTodo.js
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { addTodo } from './todosSlice'

// AddTodo component に props として addTodo の Action を渡すため
const mapDispatch = { addTodo }

const AddTodo = ({ addTodo }) => {
  const [todoText, setTodoText] = useState('')

  const onChange = e => setTodoText(e.target.value)

  return (
    <div>
      <form
      {
        // 送信ボタンが押された時、form の値がなければそのまま return
        // 値があれば addTodoの Action が Reducer にフォームの値を送り、 state が更新される、 
        // その後、フォームは空になる
      }
        onSubmit={e => {
          e.preventDefault()
          if (!todoText.trim()) {
            return
          }
          addTodo(todoText)
          setTodoText('')
        }}
      >
        <input value={todoText} onChange={onChange} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  )
}

// connectを使って　Action を AddTodo に Propsとして送る
export default connect(
  null,
  mapDispatch
)(AddTodo)

~~~


### 表示するTODOの切り替え部分の実装

~~~jsx:features/filters/filterSlice.js
import { createSlice } from '@reduxjs/toolkit' // createSlice を使えるようにする

// フィルタリングの状態(全て、完了、未完了)を定義した定数
export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}


const filtersSlice = createSlice({
  name: 'visibilityFilters', // slice の名前を設定
  initialState: VisibilityFilters.SHOW_ALL, // slice の初期値を設定
  reducers: { // コールバックは使わず通常の書き方
    setVisibilityFilter(state, action) {
      return action.payload // 受け取った action の値でそのまま state を更新
    }
  }
})

// Action を export
export const { setVisibilityFilter } = filtersSlice.actions

// Reducer を export
export default filtersSlice.reducer
~~~

~~~jsx:features/filters/Link.js
import React from 'react'
import PropTypes from 'prop-types'

// filter には VisibilityFilters のいずれかが入る 
const Link = ({ active, children, setVisibilityFilter, filter }) => (
  <button
    // setVisibilityFilter Action で Reducer に filter の値を送信
    onClick={() => setVisibilityFilter(filter)}
    disabled={active} // 現状クリックされているなら disabled に
    style={{
      marginLeft: '4px'
    }}
  >
    {children}
  </button>
)

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  setVisibilityFilter: PropTypes.func.isRequired,
  filter: PropTypes.string.isRequired
}

export default Link

~~~


### Storeから State を受け取り表示する部分の実装

~~~jsx:src/features/todos/VisibleTodoList.js
import { connect } from 'react-redux'
import { createSelector } from '@reduxjs/toolkit'
import { toggleTodo } from 'features/todos/todosSlice'
import TodoList from './TodoList'
import { VisibilityFilters } from 'features/filters/filtersSlice'

const selectTodos = state => state.todos // Store から todos の state を select
const selectFilter = state => state.visibilityFilter //  Store から visibilityFilter の state を select

const selectVisibleTodos = createSelector( // selector を作成
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case VisibilityFilters.SHOW_ALL:
        return todos
      case VisibilityFilters.SHOW_COMPLETED:
        return todos.filter(t => t.completed)
      case VisibilityFilters.SHOW_ACTIVE:
        return todos.filter(t => !t.completed)
      default:
        throw new Error('Unknown filter: ' + filter)
    }
  }
)

const mapStateToProps = state => ({
  todos: selectVisibleTodos(state)
})

const mapDispatchToProps = { toggleTodo }

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)
~~~

# まとめ

- `createSlice` を使うと `State, Action, Reducer` を一気に作れるので便利。
- `Component` に `props` として `State` や `Action` を渡したい場合は、
`connect, mapStateToProps, mapDispatchToProps`を使うと便利。
- `Reducer` を `combineReducer` で結合させて `rootReducer` を作る
- `configureStore` で `store` を定義する。
- 作った `store` は `Provider` 経由で 子コンポーネントに渡す。