はじめに
Redux、、、難しい。。。
React/Reduxの学習を始めたフロントエンド初心者の方なら誰しも少なからず感じるはず。。。
Redux Toolkitの導入により開発が大分楽になってくるのは分かるが、それでも、やっぱり初学者にとって、役割ごとに書くコードが多い気がする。ただ、確かに、Redux Toolkitを使って開発することによって、Reduxの定型文がすっきり書けることも、書き比べることによって少しずつ実感できてきた。
...という事で、個人的な記憶の定着の意味合いが強いのですが、Redux Toolkitを使ってれば必ず使用するであろう、主要なメソッドをまとめました。
詳しくは公式ドキュメントを。
そもそもRedux Toolkitって?
Reduxの開発チームが効率的で快適なDXを目指して開発したライブラリです。
Reduxは、最小限の機能しか持っていないため、導入するためには、役割ごとに各モジュールを各自で準備しないといけません。
それは、たとえ、Reduxに習熟したとしても、必要になる定型文のようなコードをいちいち書かなければいけない事を意味します。また、アプリの規模が大きくなってくると、様々な種類のデータをどのように持つか、Storeの分割をどのようにするかということも、各自、各チームで判断しないといけなくなります。(Ducksやre-ducks等のいくつかのパターンが提案されているのみ)
上記の課題を解決する上で、Redux Toolkitを導入することは、生産性の向上に繋がるとして、注目されているように感じます。では、主要なAPIの構文を見ていきましょう。
configureStore
通常、ReduxStoreを作成するには、createStore()を呼び出してReducer関数を渡していましたが、Redux Toolkitでは、configureStore()
を使用します。
createStore
との違いは、複数の引数を渡すのでなく、名前付きフィールドを持つ単一のオブジェクトを渡す点です。reducerという名前のフィールドとして渡す必要がある点に注意が必要です。
//Redux
const store = createStore(counter)
//Redux Toolkit
const store = configureStore({
reducer: counter
})
createAction
createActionは引数にActionTypeの文字列を受け取り、その文字列のAction Creator関数を返します。既存の方法でAction Creatorを作るよりも、短くシンプルに書くことができます。
function createAction(type, prepareAction?)
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
function incremnetOriginal() {
return { type: INCREMENT };
}
function decrementOriginal() {
return { type: DECREMENT };
}
console.log(incremnetOriginal(), decrementOriginal());
// { type: 'INCREMENT' } { type: 'DECREMENT' }
// createAction関数を使うことによって上記と同じ処理を短くかけます。
const incrementNew = createAction<number | undefined>('INCREMENT');
const decrementNew = createAction<number | undefined>('DECREMENT');
console.log(incremnetNew(), decrementNew());
// { type: 'INCREMENT' } { type: 'DECREMENT' }
ReducerでActionTypeの文字列を参照する場合は、以下2パターンの方法で書くことができます。
const incremnet = createAction<number | undefined>('INCREMENT');
// ①
console.log(incremnet.toString()); // "INCREMENT"
// ②
console.log(increment.type); // "INCREMENT"
Action Creatorは引数無しで呼び出すことも、ActionにアタッチするPayloadを指定して呼び出すことも出来ます。
let action = increment();
// { type: 'INCREMENT' }
action = increment(10);
// { type: 'INCREMENT', payload: 10 }
prepare callbackについて
デフォルトでは、生成されたAction Creatorは単一の引数を受け入れ、それが、action.payloadになります。しかし、Action Creatorの複数のパラメータを受け入れたり、ランダムなIDを生成したり、タイムスタンプを取得したりするなど、payloadの生成をカスタマイズするために、追加のロジックを書きたい場面があったりします。これを実現するためには、createAction関数の第二引数に、payload値を構築するために使用される、prepare callbackを指定することができます。
prepare callbackが指定された場合、Action Creatorの全ての引数は、prepare callbackに渡され、payloadフィールドを持つオブジェクトを返します。
import { createAction, nanoid } from '@reduxjs/toolkit';
const addTodo = createAction('todos/add', function prepare(text: string){
return {
payload: {
text,
id: nanoid(),
createdAt: new Date.toISOString(),
}
}
});
console.log(addTodo('Study for App'))
/**
*{
* type: 'todos/add',
* payload: {
* text: 'Study for App',
* id: '4JIureoiO104Yijd',
* createdAt: 2020-10-11T07:53:36.581Z'
* }
*}
**/
matchメソッドについて
生成されたAction Creatorは.match(action)メソッドを持っています。
これは、渡されたActionがActionCreatorによって作成されたActionと同じTypeかどうかを判定するために使用することが可能です。
import { createAction, Action } from '@reduxjs/toolkit'
const increment = createAction<number>('INCREMENT')
function someFunction(action: Action) {
if (increment.match(action)) {
{/* 何らかの処理 */}
}
}
createReducer
通常、ReduxのReducerは、action.typeフィールドをチェックして、ActionTypeごとに特定のロジックを実行していました。
function counter(state = 0, action) {
switch (action.type) {
case increment.type:
return state + 1
case decrement.type:
return state - 1
default:
return state
}
}
Redux Toolkitでは、createReducer関数を使用して以下のように書くことができます。ActionTypeの文字列をKeyとして使用する必要があるので、ES6の「computed property」構文を使用して、ActionType文字列からKeyを作成することが出来ます。
const increment = createAction('INCREMENT');
const decrement = createAction('DECREMENT');
const counter = createReducer(0, {
[increment.type]: state => state + 1,
[decrement.type]: state => state - 1
})
以下のような書き方も可能です。
computed propertiesは、内部にあるどのような変数に対してもtoString()を呼び出すので.typeフィールドを使用せずに、ActionCreator関数を直接使用することも可能です。
const counter = createReducer(0, {
[increment]: state => state + 1,
[decrement]: state => state - 1
})
createSlice
上記でAction Creatorを個別に作成していますが、createSliceを使用する事によって、State,Reducer,Action Creatorはまとめて作成することが可能です。
createSliceは、reducerとactionsを格納したsliceオブジェクトを返します。
createSliceは、以下のオプションを持つオブジェクトパラメータを受け取ります。
function createSlice({
// ActionTypeでプレフィックスとして使用される名前
name: string,
// Reducerで使用される初期値
initialState: any,
// reducersオブジェクト。Key名からActionを生成します。
reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>
// Reducerを追加するために使用されるコールバックまたは、reducersの追加オブジェクト。
extraReducers?:
| Object<string, ReducerFunction>
| ((builder: ActionReducerMapBuilder<State>) => void)
})
reducersフィールドは、caseReducer関数(特定のActionTypeを扱うことを目的とした関数で、Switch構文内のcase文に相当)を含むオブジェクトです。KeyはActionTypeを生成するために使用されます。
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: state => state + 1,
decrement: state => state - 1
}
})
const store = configureStore({
reducer: counterSlice.reducers
})
document.getElementById('increment').addEventListener('click', () => {
store.dispatch(counterSlice.actions.increment())
})
大体は、Destructuring assignmentを使用して、以下のように、Action CreatorやReducerを取り出すことになるかと思います。
const { actions, reducer } = counterSlice;
const { increment, decrement } = actions;
ActionCreatorのpayloadをカスタマイズする必要がある場合、prepareフィールドにコールバック関数を定義します。この場合、reducer
とprepare
、二つのプロパティを含めなければいけません。
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import nanoid from 'nanoid'
interface Item {
id: string
text: string
}
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<Item>) => {
state.push(action.payload)
},
prepare: (text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
},
},
})
さいごに
公式が推奨する開発アプローチをまとめたRedux Style Guideでも推奨されており、実際に書いていても、記述量も減り、導入するメリットは大きいのではないかと思います。