はじめに
- こちらの記事を大いに参考にしてます。
- ある程度Reactとreduxに関しての知識がある読者を想定しております。
- Reactにおいての関心の分離については賛否両論ありますのであくまで一案であるということで。
- 筆者はredux歴がたったの二週間(執筆時点)なので頭おかしいこと書いてる可能性が大いにあります。温かい目で見て、よろしければコメントでご指摘ください。
やりたいこと
- reduxと密に関係するロジックのほとんどをcustom hookとして別ファイルに置いといて他コンポーネントでも使えるようにしたい。
- コンポーネント内でdispatchの文字を何度も見たくない。dispatchを呼び出すのはcustom hooks内からだけにしたい。
開発環境
- TypeScript(3.7.2)
- react(16.12.0)
- redux(4.0.5)
- react-redux(7.2.0)
使わないもの
Middleware
Middlewareの詳しい説明は省きますが(なにせ自分自身がよく理解できてないので)簡単に言うとreduxを使う上で非同期処理の扱いを助けてくれるやつです。そんな便利そうなものを使わずにどう非同期処理を扱うのかというと、custom hookに丸投げします。
Action creator関数
TypeScriptの型定義で事足りると思い使わないことにしました。
Action creatorの中でやりたい処理に関しては、custom hooksに丸投げします(二回目)。
###Redux ToolKit
便利そうですがまだ学習してないので今回はスルー。
丸投げってなんやねん
ごもっともです。これからその丸投げの中身を、おなじみカウンターの例を用いて解説していきたいと思います。
storeの設定
import { createStore } from 'redux'
type State = {
count: number
}
type Increment = {
type: 'INCREMENT'
}
type Decrement = {
type: 'DECREMENT'
}
type CounterActions = Increment | Decrement
const initialState:State = {
count: 0,
}
const reducer = (state = initialState, action: CounterActions) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 }
case 'DECREMENT':
return { ...state, count: state.count - 1 }
default:
return state
}
}
export default createStore(reducer)
action
の型をCounterActions
に定義した事によって、'INCREMENT'
か'DECREMENT'
以外の値をtype
プロパティに使おうとした場合エラーを吐いてくれます。
##custom hooks
###useCounter
import { useDispatch } from 'react-redux'
import { Dispatch } from 'react'
import { CounterActions } from '../redux/action'
const useCounter = () => {
const dispatch = useDispatch<Dispatch<CounterActions>>()
return {
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
}
}
export default useCounter
dispatch
はcustom hook内で呼び出しているのでコンポーネント側からはただincrement()
と呼ぶだけです。
普段action creator関数内で行う処理もこれらの関数の中で行います。
機能追加もここの関数をいじるだけで簡単にできてうれしい。
###useAsyncCounter
import { useDispatch } from 'react-redux'
import { Dispatch } from 'react'
import { CounterActions } from '../redux/action'
const useAsyncCounter = () => {
const dispatch = useDispatch<Dispatch<CounterActions>>()
return {
incrementAsync: () => {
setTimeout(() => dispatch({ type: 'INCREMENT' }), 3000)
},
decrementAsync: () => {
setTimeout(() => dispatch({ type: 'DECREMENT' }), 3000)
},
}
}
export default useAsyncCounter
非同期処理もこれらの関数内で行います。
##コンポーネント
###App
import React from 'react'
import { Provider } from 'react-redux'
import store from './store'
import CounterDisplay from './components/CounterDisplay'
import CounterButtons from './components/CounterButtons'
const App = () => {
return (
<Provider store={store}>
<div id='app'>
<CounterDisplay />
<CounterButtons />
</div>
</Provider>
)
}
export default App
###CounterDisplay
import React from 'react'
import { useSelector } from 'react-redux'
import { State } from '../redux/action'
const CounterDisplay = () => {
const count = useSelector((state: State) => state.count)
return <div id='display'>{count}</div>
}
export default CounterDisplay
###CounterButtons
import React from 'react'
import useCounter from '../hooks/useCounter'
import useAsyncCounter from '../hooks/useAsyncCounter'
const CounterButtons = () => {
const { increment, decrement } = useCounter()
const { incrementAsync, decrementAsync } = useAsyncCounter()
return (
<div id='buttons'>
<button className='increment' onClick={() => increment()}>
+
</button>
<button className='decrement' onClick={() => decrement()}>
-
</button>
<button className='increment' onClick={() => incrementAsync()}>
async +
</button>
<button className='decrement' onClick={() => decrementAsync()}>
async -
</button>
</div>
)
}
export default CounterButtons
onClickの中身がスッキリしてて、Foo↑気持ちぃ~
##おわりに
この手法を使ってて気になったのが、custom hookから返された関数をuseEffect内で使った際に意図しない形で何度も呼ばれてしまったりすることですが、これらはcustom hook内で返す関数をuseCallbackに包んであげればほぼ解決します。
最初はconnectやthunkを使って書いていたのですが、ボイラープレートの多さと、HOCによるコンポーネントツリーの汚染、コンポーネントファイルにいらんコードが増えるのにモヤッとしたので代替案を探してみました。
Redux ToolKitもこれらの問題を解決するのに役立ちそうなのでそろそろ手を出してみようとおもいます。