React の dispatch を分割して部分的にステートを書き換える


ステートに object を入れると dispatch しにくい

例として、<Main /><Article /><Section /> の階層構造を持つ React アプリケーションを考えます。

<Main />
 ・<Article />
  ・<Section />

React 以外のステート管理ライブラリを使わず、useContext によるグローバルステートも使わない場合、以下のようにステート管理を書けます。

import { memo, useEffect, useState } from "react";

const Main = memo(() => {
  // useReducer でも可。
  const [main, dispatchMain] = useState({
    article: {
      section: {
        content: "initial section content"
        // 他のプロパティ
      }
      // 他のプロパティ
    }
    // 他のプロパティ
  });

  return <Article article={main.article} dispatchMain={dispatchMain} />;
});

const Article = memo(({ article, dispatchMain }) => (
  <Section section={article.section} dispatchMain={dispatchMain} />
));

const Section = memo(({ section, dispatchMain }) => {
  useEffect(() => {
    setTimeout(
      () =>
        dispatchMain((prevMain) => ({
          ...prevMain,
          article: {
            ...prevMain.article,
            section: {
              ...prevMain.article.section,
              content: "Section content is updated."
            }
          }
        })),
      3000
    );
  }, [dispatchMain]);

  return <>{section.content}</>;
});

export default Main;

dispatchMain を子コンポーネントに渡すことで、子コンポーネントからステートを書き換えられるようになりました。この例では簡単に useState を使用していますが、代わりに useReducer を使って reducer 関数を自力が書けば、ステートの変化を <Main /> からでも制御できます。

しかし…… main.article.section.content を書き換えるために dispatchMain を使うの、冗長ではないでしょうか?

dispatchMain((prevMain) => ({
  ...prevMain,
  article: {
    ...prevMain.article,
    section: {
      ...prevMain.article.section,
      // ここを書き換えたいだけなのに、冗長な dispatch を書いている。
      content: "Section content is updated."
    }
  }
}));

そこで、dispatchMain から dispatchArticle を作り、さらに dispatchArticle から dispatchSection を作ります。

dispatch を分割して部分的にステートを書き換える

以下のようにして、dispatchArticledispatchSection を作れます。

const dispatchArticle = useCallback(
  (action) =>
    dispatchMain((prevMain) => ({
      ...prevMain,
      article:
        typeof action === "function" ? action(prevMain.article) : action
    })),
  []
);

const dispatchSection = useCallback(
  (action) =>
    dispatchArticle((prevArticle) => ({
      ...prevArticle,
      section:
        typeof action === "function" ? action(prevArticle.section) : action
    })),
  [dispatchArticle]
);

main.article.section.content を書き換えるコードは、以下のようにすっきり書けます。

dispatchSection((prevSection) => ({
  ...prevSection,
  // すっきりと書ける。
  content: "Section content is updated."
}));

全体のコードはこちら

「dispatch を分割するパターン」が広まってほしい

この「dispatch を分割するパターン」、私は好きなのですが、インターネット上には全く事例がない。素直にステート管理ライブラリを使うべきなのか、それとも React だけでシンプルにステート管理するべきなのか……。useContext でグローバルステートを作ると、レンダリングパフォーマンスの管理が難しいしな……。

「dispatch を分割するパターン」が認知されると、メリット・デメリットも見えてきて、うれしいですね。