メインコンテンツまでスキップ

Redux - Toolkit

QuickStart

目的

  • Redux Toolkit は、redux に関する下記の問題を解決するためのツール
    • 初期セットアップが複雑すぎる
    • 便利に使うには多くのパッケージをインストールする必要がある
    • ボイラープレートが多すぎる
  • 全てのケースはカバー出来なくても、大半のケースでコードを簡略化できるツールを目指している
  • create-react-appapollo-boostの精神に感化されている

含まれるもの

  • configureStore()
  • createReducer()
  • createAction()
  • createSlice()
  • createAsyncThunk()
  • createEntityAdapter
  • createSelector --- reselectcreateSelectorを利便性のために再エクスポートしたもの

基本チュートリアル

configureStore

  • createStore()のラッパー
  • reducer をオブジェクトとして与えた場合にはcombineReducer()が自動で呼ばれる
  • ミドルウェア群がデフォルトで追加される
    • Redux DevTools
    • redux-thunk
    • Immutability check middleware --- store の値が直接改変されないよう監視する
    • Serializability check middleware --- シリアライズ出来ない値()が store に入り込まないように監視する
// Before:
const store = createStore(counter);

// After:
const store = configureStore({
reducer: counter,
});

createAction

  • 与えられた action type (文字列)を元に action creator を生成する
  • 生成された action creator 関数はtoString()メソッドを持つため、そのまま action type としても使用できる
  • 本当はcreateActionCreator()正しい名前である

action type の取得方法は 2 つある

  • オーバーライドされたtoString()を使う(action creator をそのまま使う)
  • .typeを使う
// Before
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

function increment() {
return { type: INCREMENT };
}

function decrement() {
return { type: DECREMENT };
}

function counter(state = 0, action) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
}

// After
const increment = createAction('INCREMENT');
const decrement = createAction('DECREMENT');

function counter(state = 0, action) {
switch (action.type) {
// .typeを使う方法
case increment.type:
return state + 1;
// toStringを使う方法
case decrement:
return state - 1;
default:
return state;
}
}

createReducer

  • slice reducer を作成して返す
    • root reducer --- store 全体を管理する reducer (アプリ単位。多くの場合combineReducers()で作られる reducer)
    • slice reducer --- store の一部を管理する reducer (ドメイン単位。例:store.todosを管理する reducer)
    • case reducer --- 個々のアクションに対応する reducer (アクション単位。例:ADD_TODO を担当する reducer)
  • action type から reducer へのルックアップテーブルという形で処理を記述できるようにする。これにより switch 文が不要になる。
  • 裏側で immer が使われているため、state を直接書き換える形で値を改変することができることにより、簡潔な表記が可能になる
  • デフォルトケースについては明記する必要はない
  • TypeScript 環境では型推論の効く builder パターンでの記述を推奨
const increment = createAction('INCREMENT');
const decrement = createAction('DECREMENT');

// object記法
// - action変数に手動で型付けが必要
// - アクション名を参照するときに`.type`が必要になる場合あり
const counter = createReducer(0, {
// toStringを使う方法
[increment]: (state, action) => {},
// .typeを使う方法
[decrement.type]: (state, action) => {},
});

// builder記法
// - action変数には自動で型推論が効いている
// - アクション名を参照するときに`.type`は不要
const counter = createReducer(0, (builder) =>
builder
.addCase(increment, (state, action) => {})
.addCase(decrement, (state, action) => {}),
);

createSlice

  • slice オブジェクトを作成する(state の一部を管理する責務をもつオブジェクト)
  • 下記を一括で生成する
    • reducer
    • action creators (reducer オブジェクトのキー名が関数名となる)
    • action type strings (slice 名 + reducer オブジェクトのキー名)
  • 引数
    • slice 名
    • store の初期値
    • reducers オブジェクト
  • 多くの場合createAction()createReducer()を使うまでもなくcreateSlice()だけで事足りる
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => (state += 1),
decrement: (state) => (state -= 1),
},
});

const { actions, reducer } = counterSlice;
const { increment, decrement } = actions;

// createSlice()の返値は下記のような形式になる
// {
// name: "todos",
// reducer: (state, action) => newState,
// actions: {
// addTodo: (payload) => ({type: "todos/addTodo", payload}),
// toggleTodo: (payload) => ({type: "todos/toggleTodo", payload})
// },
// caseReducers: {
// addTodo: (state, action) => newState,
// toggleTodo: (state, action) => newState,
// }
// }

extraReducers

  • action creator を自動的に作成したくない場合に使用する
  • action creator が作成されない点を除いてreducersと同じ働きをする
  • TypeScript 環境では型推論の効く 前述の builder 記法での記述を推奨
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
// object記法
extraReducers: {
[increment]: (state, action) => {},
[decrement]: (state, action) => {},
},
// builder記法
extraRducers: (builder) =>
builder
.addCase(increment, (state, action) => {})
.addCase(decrement, (state, action) => {}),
});

注意点

createSlice や ducks パターンで注意すること

  • action type は単一の slice でのみ使うのではなく、複数の slice で共有されるべきものであることを認識して設計を行う
    • 例えば、ユーザがログアウトしたよー、というアクションは様々な slice ので処理されうる
  • action type の循環参照に注意する。対処法は以下の通り。
    • 例えば 2 つ異なる slice がお互いの action (type) を相互に参照している場合
    • 該当する action を新たなファイルに移行してcreateActionで作成する
    • 作成した action type を 2 つの slice においてインポートしextraReducerに処理を記載する

createAsyncThunk

  • createAsyncThunk を使うと、thunk を書くときにありがちな冗長なコードを簡略化できる。
  • 与えられた action type (文字列)と Promise を返す関数を元に、thunk を生成する
  • thunk は Promise の結果によりpending/fulfilled/rejectedのいずれかの action type を送出する
  • 生成された thunk はthunk.fulfilled等の形式で action type として利用できる
export const fetchIssue = createAsyncThunk(
'issues/fetchIssue', // アクション名は手動で指定する
async (arg, thunkAPI) => {
// 引数は1つしか使えないので、複数必要な場合はこのようにオブジェクトにする必要がある
const { org, repo, issueId } = arg;
const issues = await getIssue(org, repo, issueId);
return issues;
},
);

const issues = createSlice({
name: 'issues',
initialState: issuesInitialState,
reducers: {},
// action creatorは作成する必要がないので、extraReducersを使う
extraReducers: {
[fetchIssue.pending.type]: (state, action) => {},
// action.payloadにissuesが入っている
[fetchIssue.fulfilled.type]: (state, action) => {},
[fetchIssue.rejected.type]: (state, action) => {},
},
});

// コンポーネントで
dispatch(fetchIssue({ org: 1, repo: 2, issueId: 3 }));

なお、ThunkAPIは以下のような値を持つ

interface ThunkAPI {
dispatch: Function;
getState: Function;
extra?: any;
requestId: string;
signal: AbortSignal;
}

unwrapResult

  • 通常の thunk action と異なり、createAsyncThunk で作成した thunk action は常に action{type, payload}を返す。
  • この挙動を避けて、元のプロミスが返す値を取得したい場合は、unwrapResultを使う。
import { unwrapResult } from '@reduxjs/toolkit';
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then((fetchedUser) => {});
};

createEntityAdapter

  • state を正規化して管理している場合に役立つツール
  • entity を追加・更新・削除するための、定型的な reducers を簡単に作成できる
  • reselectでメモ化された、定型的な selectors を簡単に作成できる

entity のデータ構造

createEntityAdapterを使うと、個々の entity はidsentitiesで管理されるようになる

// stateの構成例
{
entities {
books: {
ids: [1,2,3]
entities: {1:{},2:{},3:{}}
}
authors: {
ids: [1,2,3]
entities: {1:{},2:{},3:{}}
}
}
}

アダプタの作成

type Book = { bookId: string; title: string };

const booksAdapter = createEntityAdapter<Book>({
// idフィールドに`id`キー以外のものを使いたい場合は下記のように明示的に指定する
selectId: (book) => book.bookId,
// 並べ替えの方法を指定したい場合
sortComparer: (a, b) => a.title.localeCompare(b.title),
});

initialState の作成

getInitialState()は単に{ids:[],entities:{}}を返す

const initialState = booksAdapter.getInitialState();

case reducers の利用

アダプタは entity を CRUD するための便利な関数を持つ。シグネチャはメソッドごとに微妙に異なるので、都度ドキュメントを参照すること。

  • addOne
  • addMany
  • setAll
  • removeOne
  • removeMany
  • updateOne
  • updateMany
  • upsertOne
  • upsertMany
const booksSlice = createSlice({
name: 'books',
initialState,
reducers: {
// アダプタのreducerをそのまま使用する場合
bookAdded: booksAdapter.addOne,
// アダプタのreducerを手動で呼ぶ場合
booksReceived(state, action) {
booksAdapter.setAll(state, action.payload.books);
},
},
});

セレクタ

  • selectIds
  • selectEntities
  • selectAll
  • selectTotal
  • selectById
// グローバル化されていないセレクタには、entityだけを明示的に渡す必要がある
const nonGlobalizedSelectors = booksAdapter.getSelectors();
const allBooks = useSelector((state) =>
nonGlobalizedSelectors.selectAll(state.enties.books),
);

// グローバル化されたセレクタには、root stateをそのまま渡せる
// つまり、useSelectorにそのまま渡せる
const globalizedSelectors = booksAdapter.getSelectors<RootState>(
(state) => state.books,
);
const bookIds = useSelector(globalizedSelectors.selectAll);

中級チュートリアル

ducks パターン

redux コミュニティの慣例

  • 単一ファイルに action creators と reducers を記載する
  • reducer をデフォルトエクスポートする
  • action creators を named export する

おすすめのフォルダ構成

  • ほとんどの場合において "feature folder" approach が有効であることが確認されている
  • 機能やドメインごとにフォルダ分けする方法
  • ファイルタイプ(actions, reducers, containers, components)ごとにフォルダを分ける方法は見通しが悪化しがち
  • 参考

フォルダ構成例

このプロジェクトを参考にするとよいかも

  • src
    • api
      • api を叩く関数など
    • app
      • App.tsx|css, rootReducer.ts, store.tsなど、アプリの核となるファイル
    • components
      • 再利用可能なコンポーネント
      • 画面や機能に依存しないコンポーネント
    • features
      • 機能ごと、画面ごと、ドメインごとにフォルダを作る
      • コンポーネント、css、スライス(actions, reducers)、セレクタ、テストファイルなどを含む
    • utils

Flux Standard Actions

  • アクションは{type:string, payload: any}の形式であるべき
  • redux-toolkit ではデフォルトでその形式になる

payload に手を加えるには

  • createAction()createSlice()を使って生成された action creators は、与えた引数をそのままaction.payloadとして送出する
  • 与えた引数に何らかの処理を行ってから(prepare してから) payload を作成したい場合は下記のようにする
// createActionの場合は第2引数に`prepare callback`を記載する
const addTodo = createAction('ADD_TODO', text => {
return {
payload: { id: uuid(), text }
}
})

// createSliceの場合はreducerとprepare functionを分けて記述する
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer(state, action) {
const { id, text } = action.payload
state.push({ id, text, completed: false })
},
prepare(text) {
return { payload: { text, id: 1 + 2 + 3 } }
}
}
}
}

セレクタの最適化(sharrowEqual を使う場合)

  • 下記の場合、data は毎回新しいオブジェクトになる。すなわち再描写がかかる。
  • これは useSelector の比較方法が reference equality だからである。なお、connect の比較方法は shallow equality である。
    • reference equality --- セレクタが返したオブジェクト「自体」のアドレスが同一であるか
    • shallow equality --- セレクタが返したオブジェクトの「1 階層目のキーの種類とその参照先アドレス」が同一であるか
  • 第 2 引数に shallowEqual を渡すことで比較方法を変更できる
const data = useSelector(
(state) => ({
commentsLoading: state.comments.loading,
commentsError: state.comments.error,
comments: state.comments.commentsByIssue[issueId],
}),
// shallowEqual,
);

セレクタの最適化(reselect を使う場合)

  • shallowEqual を使ったとしても再描写がかかってしまう場合がある。例えば下記のgetVisibleTodos()のうち.filter()された結果については、必ず新しい(=参照の異なる)配列として生成される
  • このように state の一部をフィルタして抜き出すなどするときなどは、reselectを使って適宜メモ化すること
  • reselectを使うと.filter()した結果もメモ化され、同じ参照のオブジェクトとして取得できる
import { connect, useSelector } from 'react-redux'
+import { createSelector } from '@reduxjs/toolkit'
import { toggleTodo } from 'features/todos/todosSlice'
import TodoList from '../components/TodoList'
import { VisibilityFilters } from 'features/filters/filtersSlice'

-const getVisibleTodos = (todos, filter) => {
- switch (filter) {
- case VisibilityFilters.SHOW_ALL:
- return todos
- case VisibilityFilters.SHOW_COMPLETED:
- return todos.filter(t => t.completed)
- case VisibilityFilters.SHOW_ACTIVE:
- return todos.filter(t => !t.completed)
- default:
- throw new Error('Unknown filter: ' + filter)
- }
-}
+const selectTodos = state => state.todos
+const selectFilter = state => state.visibilityFilter
+const selectVisibleTodos = createSelector(
+ [selectTodos, selectFilter],
+ (todos, filter) => {
+ switch (filter) {
+ case VisibilityFilters.SHOW_ALL:
+ return todos
+ case VisibilityFilters.SHOW_COMPLETED:
+ return todos.filter(t => t.completed)
+ case VisibilityFilters.SHOW_ACTIVE:
+ return todos.filter(t => !t.completed)
+ default:
+ throw new Error('Unknown filter: ' + filter)
+ }
+ }
+)


const mapStateToProps = state => ({
- todos: getVisibleTodos(state.todos, state.visibilityFilter)
+ todos: selectVisibleTodos(state)
})

又はhookの場合、
+ const todos = useSelector(selectVisibleTodos)

上級チュートリアル

お題のプロジェクト

dispatch()の戻り値

  • dispatch()の戻り値は引数の戻り値と等しくなる。例えば:
    • プレーンなaction creator()を与えた場合 --- action creator が return した値(通常は{type, payload})
    • thunk action creator()を与えた場合 --- thunk が return した値(よって、thunk が async なら Promise が戻る)

HMR

  • HMR が利用できる環境ではmodule.hotが存在するので、これを使って再描写を行う
  • 再描写の方法はaccept()のコールバックに個別に記載する
  • create-react-app ではデフォルトでは HMR ではなくフルリロードが行われる
module.hot.accept(
dependencies, // 監視するファイル
callback, // ファイルが変更されたときに何をするか
);

store の型

  • mapState,useSelector,getStateなどで store の型を利用するにはReturnType という TS のビルトイン型を使う
  • 下記のようにすることで自動的に store の型が最新に保たれる
// app/rootReducer.ts

import { combineReducers } from '@reduxjs/toolkit';
const rootReducer = combineReducers({});

// rootReducerが返す値の型をstoreの型として利用する
// 型はエクスポートしておき、各所で利用する
export type RootState = ReturnType<typeof rootReducer>;

export default rootReducer;

store のセットアップ

app/store.tsのセットアップ例

インポート関連

import { Action, configureStore } from '@reduxjs/toolkit';
import {
TypedUseSelectorHook,
useDispatch as rawUseDispatch,
useSelector as rawUseSelector,
} from 'react-redux';
import { ThunkAction } from 'redux-thunk';
import rootReducer, { RootState } from './rootReducer';

ストアの作成

const store = configureStore({
reducer: rootReducer,
});
export default store;

HMR の設定(redux 関連)

// reducerが更新されたときはHMRする
// (store.replaceReducerを使って、reducerだけを入れ替える)
if (process.env.NODE_ENV === 'development' && module.hot) {
module.hot.accept('./rootReducer', () => {
const newRootReducer = require('./rootReducer').default;
// const newRootReducer = (await import('./rootReducer')).default

store.replaceReducer(newRootReducer);
});
}

thunk action の型定義

  • thunk action creator の戻り値の型(= thunk action の型)として使用する
  • createAsyncThunkのみを使用し、手動で thunk を作成しない場合は、この作業は不要
export type MyThunkAction<R = Promise<any>> = ThunkAction<
R, // thunk actionの戻り値(結果)の型。ほとんどの場合Promiseになる。
RootState, // root stateの型
unknown, // thunk actionの第3引数の型(拡張用、通常は使わない)
Action<string> // action.typeの型
>;

// コンポーネントでの使用例
export const fetchIssues = (): MyThunkAction => async (dispatch) => {};

dispatch の型定義

// 暗黙的に ThunkDispatch 型になる
type MyDispatch = typeof store.dispatch;
export const useDispatch = () => rawUseDispatch<MyDispatch>();

useSelecter の型定義

  • 素の useSelector を型付けして再利用可能にしておく
export const useSelector: TypedUseSelectorHook<RootState> = rawUseSelector;

起点ファイル(index.ts)のセットアップ

// index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './app/store';
import './index.css';

const render = () => {
const App = require('./app/App').default;
// const App = (await import('./app/App')).default;

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
};

// 初回に一度だけレンダリングを行う
render();

// コンポーネントが更新されたときはHMRする
// (React部分だけを再描写する)
if (process.env.NODE_ENV === 'development' && module.hot) {
module.hot.accept('./app/App', render);
}

どのような値を redux で管理すべきか

  • 最適
    • 複数のコンポーネントにまたがって使用されそうな値
  • 不適
    • 一つのコンポーネントでのみ使用される値、特にフォームの値

reducers や actions の型

createSlice()では下記の 2 箇所で型を指定できる

  • initialState
    • 各 case reducer が受け取る state の型として利用される
    • slice reducer が返す値の型として利用される。つまり、最終的に store の型として利用される
  • case reducer の action
    • 各 case reducer が受け取る payload の型として利用される
    • action creator に渡すべき引数の型として利用される
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

let initialState: SomeMyType = {
org: 'rails',
repo: 'rails',
page: 1,
displayType: 'issues',
issueId: null,
};

const issuesDisplaySlice = createSlice({
name: 'issuesDisplay',
initialState,
reducers: {
setCurrentPage(state, action: PayloadAction<number>) {
state.page = action.payload;
},
},
});

thunk とは

// これは`action` creator
function exampleActionCreator() {
// これは`action`
return { type: SOME_TYPE, payload: '' };
}
store.dispatch(exampleActionCreator());

// これは`thunk action` creator --- 略してthunkと呼ぶ
function exampleThunk() {
// これは`thunk action`
return function exampleThunkFunction(dispatch, getState) {
// dispatch(plainActionCreator())
};
}
store.dispatch(exampleThunk());
  • thunk とは、関数を返す関数のこと
  • その目的は計算を後段に遅らせるため
    • 結果を得るには 2 回呼ぶ必要がある(thunk()())
    • これにより、thunk を実行するタイミングと、実際にその結果を得るタイミングをずらすことができる
  • redux-saga や redux-observable も便利だが、ほとんどの場合は thunk で事足りる
  • thunk はcreateSlice()内では作成出来ないので、その外側で独立した関数として作成し、named export する
  • thunk は slice ファイル内に記載すると良い

thunk にまつわる型

thunk にまつわる型定義は予めstore.tsなど一箇所で行っておくと、何度も書く必要がなくなるので便利。詳細は前述のstore のセットアップ項目を参照。

thunk の利点

  • ロジックが再利用可能で汎用性の高いものになる
  • コンポーネントから複雑なロジックを分離できる
  • コンポーネント内で利用する際に、同期・非同期を意識しなくてすむ
  • dispatch()awaitするなどして非同期処理の完了を知ることができる

thunk のエラーハンドリング

下記のような書き方をするとgetRepoDetailsSuccess()で起きたエラーまで拾ってしまうので、本当はもう少し丁寧な記述が必要。

try {
const repoDetails = await getRepoDetails(org, repo);
dispatch(getRepoDetailsSuccess());
} catch (e) {
dispatch(getRepoDetailsFailed());
}

Slice の例

  • Slice ファイルの全体像は下記のようになる(createAsyncThunk を使わないパターン)
  • 並びとしては以下のようになる
    • 型定義
    • inital state
    • 複数の action creators で共通して利用する case reducer 関数
    • createSlice
    • action creators の destructuring と named export
    • reducer の default export
    • thunk action creators
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getIssue, getIssues, Issue, IssuesResult } from 'api/githubAPI';
import { MyDispatch, MyThunkAction } from 'app/store';
import { Links } from 'parse-link-header';

interface IssuesState {
issuesByNumber: Record<number, Issue>;
currentPageIssues: number[];
pageCount: number;
pageLinks: Links | null;
isLoading: boolean;
error: string | null;
}

const issuesInitialState: IssuesState = {
issuesByNumber: {},
currentPageIssues: [],
pageCount: 0,
pageLinks: {},
isLoading: false,
error: null,
};

function startLoading(state: IssuesState) {
state.isLoading = true;
}

function loadingFailed(state: IssuesState, action: PayloadAction<string>) {
state.isLoading = false;
state.error = action.payload;
}

const issues = createSlice({
name: 'issues',
initialState: issuesInitialState,
reducers: {
getIssueStart: startLoading,
getIssuesStart: startLoading,
getIssueSuccess(state, { payload }: PayloadAction<Issue>) {
const { number } = payload;
state.issuesByNumber[number] = payload;
state.isLoading = false;
state.error = null;
},
getIssuesSuccess(state, { payload }: PayloadAction<IssuesResult>) {
const { pageCount, issues, pageLinks } = payload;
state.pageCount = pageCount;
state.pageLinks = pageLinks;
state.isLoading = false;
state.error = null;

issues.forEach((issue) => {
state.issuesByNumber[issue.number] = issue;
});

state.currentPageIssues = issues.map((issue) => issue.number);
},
getIssueFailure: loadingFailed,
getIssuesFailure: loadingFailed,
},
});

export const {
getIssuesStart,
getIssuesSuccess,
getIssueStart,
getIssueSuccess,
getIssueFailure,
getIssuesFailure,
} = issues.actions;

export default issues.reducer;

export const fetchIssues =
(org: string, repo: string, page?: number): MyThunkAction =>
async (dispatch: MyDispatch) => {
try {
dispatch(getIssuesStart());
const issues = await getIssues(org, repo, page);
dispatch(getIssuesSuccess(issues));
} catch (err) {
dispatch(getIssuesFailure(err.toString()));
}
};

export const fetchIssue =
(org: string, repo: string, number: number): MyThunkAction =>
async (dispatch: MyDispatch) => {
try {
dispatch(getIssueStart());
const issue = await getIssue(org, repo, number);
dispatch(getIssueSuccess(issue));
} catch (err) {
dispatch(getIssueFailure(err.toString()));
}
};

createSlice を使わないという選択

  • createSlice を使う弊害
    • createAsyncThunk()を使わない場合 --- thunk を createSlice の外部かつ後段に書く必要があり、コードのまとまりとして不自然で見にくい
    • createAsyncThunk()を使う場合 --- thunk を slice よりも前で宣言する必要があることから、必然的に slice 内で作成した action creator にアクセスすることが出来ない
    • あまりないケースだが、slice 内で作成した action creator から、同一の slice 内で作成した他の action creator にアクセス出来ない
  • 結論として、あえて createSlice を使わずに、下記のようにした方が汎用性が高く、シンプルではないか?
const sliceName = 'issuesDisplay/';

export const syncActionCreator1 = createAction<number>(
`${sliceName}syncActionCreator1`,
);
export const syncActionCreator2 = createAction<number>(
`${sliceName}syncActionCreator2`,
);
export const asyncActionCreator1 = createAsyncThunk<number, string>(
`${sliceName}asyncActionCreator1`,
async (name, { dispatch }) => {
dispatch(syncActionCreator1(100));
dispatch(syncActionCreator2(200));
return name.length;
},
);
export const asyncActionCreator2 = createAsyncThunk<void, void>(
`${sliceName}asyncActionCreator2`,
async () => {},
);

const reducer = createReducer(initialState, (builder) =>
builder
.addCase(syncActionCreator1, (state, action) => {})
.addCase(syncActionCreator2, (state, action) => {})
.addCase(asyncActionCreator1.fulfilled, (state, action) => {}),
.addCase(asyncActionCreator2.fulfilled, (state, action) => {}),
);

export default reducer;