LoginSignup
8
8

More than 3 years have passed since last update.

redux-thunk と typescript でちゃんと型定義する

Last updated at Posted at 2019-05-05

そういえば redux-thunkconnect 周りいつも適当に型つけてたので、実際ちゃんとやろうと思うとどうなるかなと思って書いてみた。案外ちゃんと書ける。いうほど型アサーションの重複もない。reducer 内の payload や、コンポーネント内に渡される thunk の関数のも補完や型チェック効くので安全度は高い。記述量は多い。

記述量は多いが、redux の管理する state とその更新パターンである reducer はクリーンに保つことができるし、細かいロジックは thunk に分離できるし、プレゼンテーションコンテナに提供されるインターフェイスも connect の定義も型チェックの保護を受けるので、受け入れられるような気がしてきた。

import React, { useCallback } from "react";
import { connect, Provider } from "react-redux";
import { createStore, applyMiddleware, combineReducers } from "redux";
import ReduxThunk, { ThunkAction } from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";

// state
type CounterState = number;
type State = { counter: CounterState };

// action creator
const actionCreators = {
  add: (num: number) => ({ type: "ADD" as const, payload: num })
};

type ActionTypes = ReturnType<typeof actions[keyof typeof actionCreators ]>;

// async action (thunk)
type ThunkPromiseAction<T> = ThunkAction<
  Promise<T>,
  State,
  undefined,
  ActionTypes
>;
const thunks = {
  add: (num: number = 1): ThunkPromiseAction<void> => async dispatch => {
    dispatch(actionCreators.add(num));
  }
};

// reducer
const counterReducer = (
  state: CounterState = 0,
  action: ActionTypes
): CounterState => {
  switch (action.type) {
    case "ADD":
      return state + action.payload;
  }
  return state;
};

const store = createStore(
  combineReducers({ counter: counterReducer }),
  composeWithDevTools(applyMiddleware(ReduxThunk))
);

// component
const Index: React.FC<{
  counter: number;
  add: (num?: number) => Promise<void>;
}> = props => {
  const handleAdd = useCallback(() => props.add(), [props.add]);
  return (
    <div>
      <p>counter {props.counter}</p>
      <button onClick={handleAdd}>+</button>
    </div>
  );
};

// redux connect
const ConnectedIndex = connect(
  ({ counter }: State) => ({
    counter
  }),
  {
    add: thunks.add
  }
)(Index);

// next.js page
export default () => (
  <Provider store={store}>
    <ConnectedIndex />
  </Provider>
);

サンプルコードは next.js で書いているけど特に依存はしていない。書いてる最中何度も thunkthank と書き損じた。

記述量の話

よし、じゃあ「減らす」アクションを追加しよう

  • アクションクリエーター sub を追加
  • thunk の sub を追加
  • reducer に SUB のパターンを追加
  • コンポーネントの props で sub を受け取るように修正
  • コンポーネントに + ボタンの横 に-を増やし、subを呼ぶようにする
  • connect で thunks.sub をマッピング

thunk が増えたところで大したことないな!

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8