# Redux - Toolkit

# QuickStart

# 目的

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

# 含まれるもの

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

# 基本チュートリアル

# 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 はidsとentitiesで管理されるようになる

// 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 の型定義

  • 素の useDispatch を型付けして再利用可能にしておく
  • これを行わないと dispatch().then()したときに型エラーとなる
  • https://qiita.com/hiroya8649/items/73d80a52636a787fefa5
// 暗黙的に 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;