Sanma Salted

半人前エンジニアによるチラシの裏

ReactのHooksでコンポーネントとReduxの橋渡しをするようにした

こんばんは。今日はReactについて書いていきます。

今担当しているブロダクトではフロントエンドをTypescript + React + Reduxで開発しています。 新規の開発だったので、コンポーネントに関してはReact公式が推奨するようにClass ComponentではなくFunctional Component + hooks で統一しています。

その中でCustom Hooksを使ってみたら個人的にとてもスッキリしたので、何が良かったかについて書いていきます

hooks / custom hooksとは何か

そもそもhooksとは何かを書こうかと思いましたが、今さら私が書くまでもなく良質な解説記事がたくさんあるのでそちらを参照していただければ。 公式を見てもらうのが一番早くてわかりやすいかなと思います。

reactjs.org

hooksを使ってコンポーネントはhooksにのみ依存、reduxへの直接の依存を避けるようにした

この記事のメインの内容になりますが 今回、コンポーネントからReduxへの直接の依存を排してコンポーネントは独自に定義したCustom Hooksにのみ依存させるという方法を試してみました。

実装のイメージとしては以下のような感じです。

export function useUserList(): User[] | null {
    // useSelectorなどreduxからデータを取得する
}

export function UserList() {
    const users = useUserList();
    
    return (
        // render....
    );
}

従来であれば下記のように、コンポーネント内で直接reduxのstateにアクセスしていたと思います。

export function UserList() {
    const users = useSelector((state) => state.user.list);
    
    return (
        // render....
    );
}

コンポーネントが直接reduxを触ることの問題点

上記の例だけだとあまり記述量が変わらないように見えます。 が、実際には多くのケースでuseEffectuseDispatchを用いてreduxのアクションを呼び出す処理が入るなどし、 コンポーネントがreduxに密に直接依存してしまっていました。

この状態の何が好ましくないかというと

という点が挙げられます。

まずは"コンポーネントの責務が大きくなりすぎる"という点です。 そもそもコンポーネントはあくまでViewであり、アプリケーションが持っている何かしらの「状態」とユーザーとの間のインターフェイスであることがその責務のはずです。 言い換えるなら、「状態」に見た目を与えてユーザーに見せ、またユーザーによる「状態」を変えるための操作を受け付けるのがその役割です。

上記の例のようにコンポーネントが直接reduxを触ってしまうと、それに加えて「アプリケーション内の状態を操作し、その変更を受け取る」という責務を併せ持つことになってしまいます。

これ自体は(単一責任原則に反していることに目をつぶれば)大きな問題ではないのかもしれませんが、次の"「状態」の「実装の詳細」に依存してしまっている"という点が加わると話が変わります。

上記の例で言えば、本来コンポーネントUser[]が欲しいだけであり、それがどこにどのように格納されているのか、あるいはどのような手段で取得されるのかは関心外であるはずです。

しかしコンポーネントが直接reduxを呼び出す場合、それがreduxのストアの特定の構造の中に配置されており、reduxのactionを通して取得されることを知らなくてはいけません。

これはすなわち、本来コンポーネントUser[]を取得できる何か(抽象)だけに依存すればいいはずなのに、そのための手段(実装の詳細)に依存してしまうことを意味します。

こうなると何が問題になるかといえば、変更に対して弱くなってしまうことです。

例えばストアの構造が変わる場合や、あるいはreduxそのものを廃したいこともあるかもしれません、そうなった際に変更の影響範囲はとても広くなってしまいますし、 そうすると変更のコストはどんどんと膨れていってしまいます。

reduxへのアクセスをhookに切り出す

そこでこれらの状態管理に関する責務をhookとして完全に切り出してしまうことで、上記のような問題を回避することができます。

コンポーネントUser[]を渡してくれるhooksにのみ依存していれば、そのための実装はhookの中だけに閉じ込めておけます。 そうなるとストアの実態がreduxであることを知っているのはhookのみになり、ストアに関する変更はhookのみに影響することになります。

また、コードの上でも状態管理のための長々とした記述をhookを呼び出すために1行に収めることができ、全体の可読性も向上します。

それとこれは余談になりますが、上記のようにコンポーネントとreduxの間にhookを挟むことでコンポーネントユニットテストもとても書きやすくなります。 redux周りのことを全て忘れてhookをモックで差し替えるだけで良くなるので。

まとめ

上記のようにコンポーネントとhookで明確に責務を分けることで、見通しが良くメンテナンス性の高いコードを書くことができました。 このやり方がベストかどうかはわかりませんが、hooksはこれまでコンポーネントが全て持ってしまっていた複数の関心をうまく分離するために非常に有用なツールであるといえそうです。

他にも有用な活用方法がないか、引き続き研究してみたいと思います。