タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

React HooksとTypeScriptを使ったRedux再実装で理解度を深める試み しましょう

Sponsored link

React Hooksでましたね。
これでクラスを使う必要がなくなってみんなハッピーです(公式で再三書かれてますが、既存のコードをHooksで書き直す必要性はないです)。

それはそうとして、useReducer という新しい仲間が増えました。
ちょっと前に追加されたContextと合わせることでReduxを置き換えることができます(置き換える必要があるかどうかは考えてはいけない)。

しかし最近気がついたのですが、そもそもReduxがどういうものなのか、雰囲気で使っている人が多いようにおもいます。
ぶっちゃけ「ドキュメントやソースコードを読めばいいやんけ」、で終了する話なのです。
とはいえReduxは特定のViewライブラリへの依存を防ぐように作られていたり、なるべく縛りを作らずに薄い実装にしてプラグインで解決していくような思想になっていたり、フレームワークというよりはフレームワークのためのフレームワーク、つまりメタフレームワーク的な感じだったり、とにかくまぁそういったあれこれでわかりにくく、なんとなく既存の実装をコピペって動くコードを作っちゃってるのかなぁとかそういう風に想像しています。

それはほんとうによくないのでこれを機にちゃんと理解しましょうね、ということですでに何番煎じかになっているかもしれませんがReact Hooksを使ってReduxを再実装してみます。

しかし完全互換のものを作ってもしょうがないので以下に上げるコンセプトに沿って実装しました。

  • Reactと密結合であることを前提にする(Hooks使っちゃってるからね)

  • TypeScriptで実装する(型は最高のコメントでありドキュメント)

  • いまのReduxが提供していないだけで、実質必須な仕組みは提供する(FSA準拠のAction、reducerのメソッドチェインサポート、TypesafeなActionCreator、非同期サポート…などなど)

出来上がったものがこちらになります。
TypeScriptや、諸々の規約を前提にしたRedux実装なのでhard-reduxです。ところでmizchi氏のつくったhard-reducer便利ですよね。

github.com

利用側のAPIは恐らくこうなるでしょう

import React, {useEffect} from 'react';
import {render} from 'react-dom';

import {createActionCreator, createReducer, createReduxContext} from 'hard-redux';

const initialize = createActionCreator('first', () => 1);
const secondAction = createActionCreator('second', (prev: number) => {
  if (prev === 10) {
    throw new Error('overflow!!!!!!!');
  }
  return prev + 1;
});

interface Store {
  count: number;
  errorMessage?: string;
}

const reducer = createReducer<Store>({count: 0})
  .case(initialize, (state, action) => ({
    count: action.payload,
    errorMessage: undefined,
  }))
  .case(
    secondAction,
    (state, action) => {
      return {
        count: action.payload,
      };
    },
    (state, err) => ({
      ...state,
      errorMessage: err.error.message,
    })
  );

const {connect, Provider} = createReduxContext(reducer);

interface ReceivedProps {
  ccc: number;
  ems: string;
}
interface OwnProps {
  name: string;
}
type AppProps = ReceivedProps & OwnProps;

const App = connect<OwnProps, ReceivedProps>(s => ({
  ccc: s.count,
  ems: s.errorMessage,
}))(function App(props) {
  useEffect(() => {
    props.dispatch(initialize());
  }, []);
  return (
    <div>
      <p>{props.name}</p>
      <p>{props.ccc}</p>
      {props.ems && <p style={{color: 'red'}}>{props.ems}</p>}
      <button onClick={() => props.dispatch(secondAction(props.ccc))}>count up</button>
    </div>
  );
});

render(
  <Provider>
    <App name="Rosy" />
  </Provider>,
  document.getElementById('app')!
);

突貫で作ってロクにテストしていないのですが、ちゃんと各所で型が効いており、またボイラープレートが削減できていることがわかるんじゃないかとおもいます。(combineReducer、Thunk、というかそもそもMiddleware、などの実装はまだやってません。眠いので)

色々と足りていませんが、Reduxの基本思想と基本機能は満たせているはずです。がっつり型を書いていますが、実装もまだ200行以下で済んでいるので読むのも楽なんじゃないかとおもいます(ちょっときたないのは勘弁な)。

眠いので今日はねますが、明日気が向いたらもう少し突っ込んだところまで解説する記事を書こうかとおもいます。

こちらからは以上です。