Redux とは
『Redux』 とは、JavaScript製の状態管理ライブラリ。
※ 「状態」とは、アプリケーションで扱う動的なデータという理解でよい。⇨ 例えば、ユーザー名とか、いいねの数が「状態」で、状態は時系列とともに変化する。
状態管理ライブラリが解決する問題は、コンポーネントをまたいだデータの共有で、React でも Vue.js でも、コンポーネントは、DOM と同様にツリー構造をなす。
複数のコンポーネントで同じデータを使いたい、というケースがあるとする。
深い階層のコンポーネントがあるデータを必要とする場合、下図のように、実際にはそのデータを利用しないコンポーネントも含めて props のバケツリレーが発生する。
これではアプリのコードを複雑にし、さらに途中のコンポーネントに無関係な props が定義される為、不必要な依存も産みやすい。
そこで考え出されたのが、状態管理ライブラリで、『ストア』と呼ばれるデータの入れ物を用意して、各コンポーネントが直接ストアとコミュニケーションし、これにより、上述の問題が解決される。
Redux の特徴
『Redux』の特徴は、コンポーネントからのアクセスに一定のルールが設けられている点である。
React におけるデータフローが props を通した親から子への流れに限定されているのも、コードをスパゲッティ化から守るための意図がある。
状態管理がグローバル変数のように濫用できてしまうと、コードにアンチパターンを仕込む結果になってしまう。
Redux は上記のような問題に配慮し、コードを予測可能にしてバグが出にくくするため、ルールを設けている。
さて、以下がコンポーネントと Redux ストアとのデータのやり取りを表した図です({} はオブジェクト、f(x) は関数であることを示しています)。
Reduxのモジュール
State
状態を格納するオブジェクト。
{ count: 0 }
Reducer
受け取った Action(後述)に応じて State を変更する関数。
正確には、新たな State を返却する関数。
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }
case 'ADD':
return { count: state.count + action.payload }
default:
return state;
}
}
Action
状態の更新を指示するオブジェクト。一般的に以下のプロパティを持ちます。
{ type: 'INCREMENT' } // typeがStateに対する操作を表す
{ type: 'ADD', payload: 3 } // payloadは任意の引数
処理の流れ
1. コンポーネントが、Action Creator を呼び出して Action を取得する。
2. 取得した Action を Reducer に渡す。 ※これを『dispatch』という。
3. Reducer は、渡された Action に応じて State を更新する。
4. コンポーネントは State に変更があれば、関連する UI を書き換える。
設計のポイント
このようなルールの重要なポイントは、コンポーネントが自由に状態(State)を書き換えられない点です。状態を更新するためには必ず Reducer に更新処理を依頼する必要があり、さらに Reducer はあらかじめ決められた Action にしか反応しない。
状態に対して定義された変更しか加えられないこのルールは、予期しない更新処理を防ぎ、コードを予測しやすくする。
また、Action Creator も不確実性の排除に一役買って、Reducer に対しては決められた属性および値を持つ Action を渡さなければならない。type の値など間違った Action を渡さないように、Action Creator を介して Action を取得する。
Redux Toolkit
Redux 導入の課題
『Redux』は最小限の機能のため、導入する際は上で説明した各モジュールを自分で準備しなくてはいけない。
※ それらのコーディングは煩雑で、初学者にはハードルになり、ある程度習得したあとも、定型文のようなセットアップコードを書くのは大変で、また、アプリの規模によってはストアを適切に分割する必要がある。
『Redux Toolkit』は、上記の問題を解決するために開発されたライブラリである。
1. Redux ToolkitとReact-Reduxのパッケージをプロジェクトに追加。
$ npm install --save @reduxjs/toolkit react-redux
2. Reduxストアの作成
src/app/store.js という名前のファイルを作成する。
Redux ToolkitからconfigureStore APIをインポートし、まずは空のReduxストアを作成し、エクスポートするところから始める。
//JavaScript
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})
//TypeScript
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
これにより、Reduxストアが作成され、また、開発中にストアを検査できるように、
Redux DevToolsエクステンションが自動的に設定される。
3. ReduxストアをReactに提供
ストアを作成したら、src/index.jsにあるアプリケーションの周りにReact-Redux Provider を置くことで、Reactコンポーネントから利用可能にすることができる。
先ほど作成したReduxストアをインポートし、Appの周りにProviderを配置し、propとしてストアを渡す。
//JavaScript //TypeScript
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
//追加
import { store } from './app/store'
import { Provider } from 'react-redux'
ReactDOM.render(
//追加
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
4.ReduxのState Sliceを作成
src/features/counter/counterSlice.js という名前のファイルを新規に追加し、そのファイルで、Redux ToolkitからcreateSlice APIをインポートする。
スライスを作成するには、スライスを識別するための文字列名、状態の初期値、状態を更新する方法を定義するための1つ以上のreducer関数が必要で、スライスを作成すると、生成されたReduxのアクションクリエイターとスライス全体のレデューサー関数をエクスポートすることができる。
Reduxでは、データのコピーを作成し、そのコピーを更新することで、すべての状態の更新をイミュータブルに記述する必要がある。しかし、Redux ToolkitのcreateSliceとcreateReducerのAPIは内部でImerを使って、正しいimmutable更新となる「mutating」更新ロジックを書くことができるようになっている。
//JavaScript
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
//TypeScript
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
5.ストアにスライスのリデューサを追加
次に、カウンタスライスからreducer関数をインポートして、ストアに追加する必要がある。reducerパラメータ内にフィールドを定義することで、その状態に対するすべての更新を処理するためにこのスライスreducer関数を使用するようにストアに指示する。
//JavaScript //TypeScript
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer,
},
})
6.ReactコンポーネントでReduxのStateとActionを使う
React-Reduxフックを使って、ReactコンポーネントがReduxストアと対話できるようになった。useSelector を使ってストアからデータを読み、useDispatch を使ってアクションをディスパッチすることができる。src/features/counter/Counter.js ファイルを作成して コンポーネントを作成し、そのコンポーネントを App.js にインポートして 内にレンダリングする。
//JavaScript
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
export function Counter() {
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
</div>
)
}
//TypeScript
import React from 'react'
import { RootState } from '../../app/store'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
export function Counter() {
const count = useSelector((state: RootState) => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
</div>
)
}
これで、「増量」「減量」ボタンをクリックすることができる。
- 対応するReduxのアクションがストアにディスパッチされます。
- カウンタースライスのレデューサーがアクションを見て、状態を更新します。
- Counterコンポーネントは、ストアから新しい状態の値を見て、新しいデータで自分自身を再レンダリングする。
学んだこと
- configureStoreでReduxストアを作成する
- configureStoreは、名前付き引数としてreducer関数を受け取ります。
- configureStoreは自動的に良いデフォルト設定でストアをセットアップする。
- ReduxストアをReactアプリケーションのコンポーネントに提供する。
- React-Redux の コンポーネントを の周りに配置する。
- Reduxストアをとして渡す。
- Reduxの "スライス "レデューサーをcreateSliceで作成する。