どうも、ぽっぽです。
Reduxを試す一番の近道は、公式のテンプレートを使うことです。これで面倒な環境構築をコマンド1つで済ますことが出来ます。
npx create-react-app redux-essentials-example --template redux
カウンターアプリを使ってみよう
テンプレートを使ってセットアップされるのはシンプルなカウターアプリです。操作するごとにカウンターをアップしたりダウンしたりして表示するだけです。
Reduxが管理しているのはカウンターです。最もシンプルなアプリケーションを使ってReduxでどうやってカウンターを管理しているかが分かるようになっています。
Reduxの開発をするなら入れておきたいのがブラウザーの拡張機能としてインストールするDevToolsです。
これはstate
の状態を可視化出来ます。アプリケーションを操作した履歴を管理し、一つ前の操作に戻った状態に戻すことなどが出来ます。デバッグで活躍するツールです。
アプリケーション構造
アプリケーションのソースは以下のような構造です。
/src
index.js
:アプリのスタートポイントApp.js
:トップレベルのReact component/app
store.js
:Reduxストアインスタンスの作成
/features
/counter
Counter.js
:カウンター用のUIを表示するReact componentcounterSlice.js
:カウンター用のRedux logic
Redux Storeの作成
app/store.js
を開いてみましょう。
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer: {
counter: counterReducer
}
})
Redux storeはRedux ToolkitのconfigureStore
を使います。configureStore
は引数にreducer
を渡します。
アプリケーションはたくさんの異なる仕様から出来ています。それぞれの仕様に対してreducer
関数があります。
{counter: counterReducr}
のようなオブジェクトを引数に渡すことは、state.counter
がRedux stateオブジェクトの一部で、counterReducer
関数がstate.counter
をどうやって更新するか管理することを表します。
Slice ReducersとActions
counterReduce
関数は、features/counter/counterSlice.js
にあります。
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
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
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
reducers
の中で定義された、increment
,decrement
,incrementByAmount
によってstate
が更新されます。createSlice
を使うことでこのようなシンプルな表現が可能になります。これでaction creater function
が意識せずに定義が終わっています。
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
Reducersのルール
- 新しい
state
は元のstate
とaction
引数に基づいてのみ計算される state
を直接変更してはならない。代わりにstate
のコピーを作って変更することで更新するimmutable updates
でなくてなはらない- 非同期処理やその他の副作用を行ってはならない
Reduxのゴールの一つにコードが予測可能であることがある。関数の出力は入力によってのみ計算される。それはコードの処理内容やテストが理解しやすいことです。
immutable updates
は予測可能のためには重要なルールです。
ReducersとImmutable Updates
存在するオブジェクトや配列の値を変更することがmutation
で変えることが出来ない値がimmutability
です。
Reduxではreducersは決してオリジナルや現在のstateの値を変更することを許しません。
// ❌ Illegal - by default, this will mutate the state!
state.value = 123
// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}
JavaScriptの配列やオブジェクトをspread oparetorsなどを使って、手動でimmutable updatesを書くことは出来ます。ただし覚えたり正しく記載するのが困難なのです。
Redux ToolkitのcreateSlice
関数は簡単な方法でimmutable updatesを書くことが出来ます。immerと呼ばれるライブラリを内部で使っていてmutatesな書き方をして安全にimmutably updateを実行します。
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}
この2つの関数はどちらも同じ値を更新しています。後者は簡単に書けるし読みやすく理解しやすいのは一目瞭然でしょう。
Thunksを使った非同期処理の書き方
thunkは非同期処理を含むことができる特別なRedux関数です。Thunksは2つの関数を使って書かれています。
- 内側のthunk関数は
dispatch
とgetState
を引数としてもちます。 - 外側のクリエーター関数は生成とthunk関数を返します。
counterSlice
からエクスポートされた次の関数はthunk action creatorの例です。
features/counter/counterSlice.js
// The function below is called a thunk and allows us to perform async logic.
// It can be dispatched like a regular action: `dispatch(incrementAsync(10))`.
// This will call the thunk with the `dispatch` function as the first argument.
// Async code can then be executed and other actions can be dispatched
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount))
}, 1000)
}
以前はthunkを使うにはRedux用プラグインとしてredux-thunk
ミドルウエアをRedux Storeに追加される必要がありました。Redux ToolkitのconfigureStore
関数は既に組み込み済みなのですぐにthunkを使えます。
サーバからデータを取得するのにAJAX呼び出しが必要なとき、thunkの中に記述します。
features/counter/counterSlice.js
// the outside "thunk creator" function
const fetchUserById = userId => {
// the inside "thunk function"
return async (dispatch, getState) => {
try {
// make an async call in the thunk
const user = await userAPI.fetchById(userId)
// dispatch an action when we get the response back
dispatch(userLoaded(user))
} catch (err) {
// If something went wrong, handle it here
}
}
}
React Hook
ReactからRedux StoreにアクセスするにはReactのカスタムフックを利用したReact-Reduxライブラリを用います。
Redux Storeから必要なデータを読み出すにはuseSelector
フックを使います。
features/counter/counterSlice.js
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = state => state.counter.value
現在のStoreからcounter valueを取得するには次のようにします。
const count = useSelector(selectCount)
ActionsをDispatchingするにはuseDispatch
フックを使います。Redux Storeからdispatch
メソッドを取得します。
const dispatch = useDispatch()
Component State and Forms
すべてのstateはRedux storeにいつも配置する必要はありません。複数のコンポーネントから利用されるグローバルなstateだけです。コンポーネント内で管理するstateは今まで通りReactで管理します。
Formsのstateなどはコンポーネント内で管理するstateです。useState
フックを使って管理しましょう。
Redux storeで管理するかどうか迷ったときは以下のことを考えましょう。
- アプリケーションの他の場所でそのデータを使うかどうか
- オリジナルデータに基づく継承されたデータのことに気を使う必要があるかどうか
- 複数のコンポーネントで同じデータを使うかどうか
- タイムトラベルデバッグのような任意の時点にstateを戻す値があるかどうか
- UIコンポーネントをホットリロードする間データを保ち続けたいかどうか
Providing the Store
useSelector
やuseDispatch
フックによってRedux storeとやり取りを説明しましたが、どうやってRedux storeをアプリケーションに摘要するかを説明します。
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'
import * as serviceWorker from './serviceWorker'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
ルートの<App>
コンポーネントをレンダリングし始めるためにReactではReactDom.render(<App />)
を呼び出します。Redux storeを引数に渡した<Provider>
を呼び出します。
これでどのReactコンポーネントからでもuseSelector
やuseDispatch
を呼び出してRedux storeにアクセスすることが出来ます。