ReactをTypeScriptで始めることが随分楽になったと感じています。Create React Appの以下のコマンドでOKです。
$ npx create-react-app my-app --template typescript
2020年2月の中頃からReduxのテンプレートが利用可能となり、以下のコマンドで始めることができます。
$ npx create-react-app my-app --template redux
# or
$ npx create-react-app my-app --template redux-typescript
このReduxテンプレートにはRedux Toolkitが使われています。https://redux-toolkit.js.org/
本格的に利用していくのはこれからですが、使わない場合と比べてここが便利だ(そうだ)というあたりを紹介します。
利点
初期設定が簡単
次のmiddlewareが設定済みとなります。
- Redux Thunk
- immutable-state-invariant
- serializable-state-invariant-middleware
Redux Thunkは非同期通信のためのライブラリで、他2つは開発用のmiddlewareでReduxで禁止しているstateの変更を検知してくれます。
また、Redux Devtools Extensionも有効になっています。
これらを自分で適用しようとすると少々面倒です。
createSliceでActionのコードは不要に
公式ドキュメントからの抜粋となります
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: state => state + 1,
decrement: state => state - 1
}
})
const store = configureStore({
reducer: counterSlice.reducer
})
const { actions, reducer } = counterSlice
const { increment, decrement } = actions
こんな感じでAction, Reducerを定義できます。これによりほぼActionのコードがなくなるのではと期待しています。今まではComponent, Action, Reducerに対してロジックが多少入ることが気になっていましたが、Component, Reducerでそれぞれの責務でロジックを実装できそうです
Reducerのネストはいつも通りな感じです。
const sliceA = createSlice({
name: "reducerA",
//...
})
const sliceB = createSlice({
name: "reducerB",
//...
})
import { combineReducers } from "@reduxjs/toolkit"
const mergeReducer = combineReducers({
reducerA: sliceA.reducer,
reducerB: sliceB.reducer,
})
非同期通信にはRedux Thunkが採用
const createUser = createAsyncThunk(
'users/createUser',
async () => {
// API処理
}
)
createAsyncThunkでは次の3つのActionを生成してくれます。
- pending
- fulfilled
- rejected
sliceではextraReducersのbuilderを利用して追加します。createAsyncThunkで定義されたActionを利用するためです。
const userSlice = createSlice({
name: "user",
initialState: {
user: null,
},
reducers: {},
extraReducers: builder => {
builder.addCase(createUser.pending, (state, action) => {})
builder.addCase(createUser.fulfilled, (state, action) => {})
builder.addCase(createUser.rejected, (state, action) => {})
}
})
import { useDispatch } from "react-redux"
function UserComponent() {
const dispatch = useDispatch()
const handleSubmit = async () => {
const resultAction = await dispatch(createUser())
// createAsyncThunkで生成されるActionと比較して処理を変更できる
if (createUser.fulfilled.match(resultAction)) {
// success
} else {
// error
}
};
return (
<div>
<form></form>
<button onClick={handleSubmit}>submit</button>
</div>
)
}
やっておいた方がいいこと
RootReducerの型定義
TypeScriptで利用する際に用意しておいた方がいいです。以下で定義しておきます。
import { combineReducers, configureStore } from "@reduxjs/toolkit"
export const rootReducer = combineReducers({
moduleA: moduleAReducer,
})
export type RootReducerType = ReturnType<typeof RootReducer>
RootとなるReducerの型定義はあると便利ぐらいの気持ちです。
useDispatchのラップ
import { configureStore } from "@reduxjs/toolkit"
const store = configureStore({
reducer: rootReducer
})
export type AppDispatch = typeof store.dispatch
import { useDispatch } from "react-redux"
function useAppDispatch(): AppDispatch {
return useDispatch<AppDispatch>()
}
createAsyncThunkで型定義のエラーを起こさないためです。
注意点
ImmerによるReducerのstateのImmutable
以下のようにstateを直接変更するようなコードを書いても問題ありません。Redux ToolkitではImmerを利用しており、実際にstateを変更しているわけではないとのことです。
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
export const slice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: state => {
state.value += 1;
}
},
});
まとめ
煩雑な設定がなくなり、ActionやReducerの実装も統一できそうです。これからReduxを始める人、そうでない人にもおすすめです