👻

[React]ContextAPIのアンチパターン

2022/01/17に公開
37

ContextAPI と useState は本来組み合わせてはいけない

ContextAPI の機能は、コンポーネントの階層を飛び越えてデータを配信することにあります。しかしここで気をつけなければならないのが、Provider に設定する値を useState で管理すると、Provider を持っているツリーが全て再レンダリングされることです。この書き方は最悪のアンチパターンです。

アンチパターン

Provider に渡す値を更新をするのに上位のコンポーネントの useState のディスパッチャーが使用しています。どれか一つでも値を更新すると、全てのコンポーネントが再レンダリングされます。ContextAPI での解説などでこの方法をよく見かけます。しかし値一つの変更で全てを再レンダリングするなら、状態管理の必要性そのものが無くなってしまいます。これはやってはならない書き方です。

import { createContext, useContext, useState } from "react";

type ValueType = { [key: string]: number | undefined };

const context = createContext<ValueType>(undefined as never);

const Component = ({ name }: { name: string }) => {
  const value = useContext(context);
  console.log(name);
  return (
    <div>
      {name}:{value[name]}
    </div>
  );
};

const Page = () => {
  const [value, setValue] = useState<ValueType>({});
  console.log("Main");
  return (
    <>
      <button onClick={() => setValue((v) => ({ ...v, A: (v["A"] || 0) + 1 }))}>
        A
      </button>
      <button onClick={() => setValue((v) => ({ ...v, B: (v["B"] || 0) + 1 }))}>
        B
      </button>
      <button onClick={() => setValue((v) => ({ ...v, C: (v["C"] || 0) + 1 }))}>
        C
      </button>
      <context.Provider value={value}>
        <Component name="A" />
        <Component name="B" />
        <Component name="C" />
      </context.Provider>
    </>
  );
};

export default Page;
  • A を押した場合に発生する出力結果

Main
A
B
C

無駄な再レンダリングを起こさないパターン

Provider には useRef で作成したオブジェクトを渡します。Context の更新はミュータブルに行うので、書き換えても再レンダリングは発生しません。再レンダリングを必要とするコンポーネントは、それぞれの state の書き換えで再レンダリングを通知します。Context が持っているデータは、子コンポーネント内のディスパッチャーとなります。

この書き方によって、ボタン A を押した場合は<Component name="A" />のみの再レンダリングとなります。

import {
  createContext,
  Dispatch,
  SetStateAction,
  useContext,
  useEffect,
  useRef,
  useState,
} from "react";

type ValueType = {
  [key: string]: Dispatch<SetStateAction<number | undefined>>;
};

const context = createContext<ValueType>(undefined as never);

const Component = ({ name }: { name: string }) => {
  console.log(name);
  const [value, setValue] = useState<number>();
  const dispatches = useContext(context);
  useEffect(() => {
    dispatches[name] = setValue;
    return () => {
      delete dispatches[name];
    };
  }, [name]);
  return (
    <div>
      {name}:{value}
    </div>
  );
};

const Page = () => {
  console.log("Main");
  const dispatches = useRef<ValueType>({}).current;
  return (
    <>
      <button onClick={() => dispatches["A"]?.((v) => (v || 0) + 1)}>A</button>
      <button onClick={() => dispatches["B"]?.((v) => (v || 0) + 1)}>B</button>
      <button onClick={() => dispatches["C"]?.((v) => (v || 0) + 1)}>C</button>
      <context.Provider value={dispatches}>
        <Component name="A" />
        <Component name="B" />
        <Component name="C" />
      </context.Provider>
    </>
  );
};

export default Page;
  • A を押した場合に発生する出力結果

A

データを Context 側に持たせつつ、無駄な再レンダリングを防ぐパターン

構造が少々複雑になりますが、同じ名前のデータを複数のコンポーネントが使用した場合も耐えられる書き方です。データを Context 側に保存しつつ、再レンダリングを促すディスパッチャーも管理します。

これによって、対応する名前のデータを使用しているコンポーネントのみが再レンダリングの対象になります。

import {
  createContext,
  Dispatch,
  SetStateAction,
  useContext,
  useEffect,
  useRef,
  useState,
} from "react";

type ValueType = {
  dispatches: { [key: string]: Set<Dispatch<SetStateAction<{}>>> };
  values: { [key: string]: number | undefined };
};

const context = createContext<ValueType>(undefined as never);

const Component = ({ name }: { name: string }) => {
  console.log(name);
  const [_, setValue] = useState<{}>();
  const { values, dispatches } = useContext(context);
  useEffect(() => {
    if (dispatches[name]) dispatches[name].add(setValue);
    else dispatches[name] = new Set([setValue]);
    return () => {
      dispatches[name].delete(setValue);
    };
  }, [name]);
  return (
    <div>
      {name}:{values[name]}
    </div>
  );
};

const Page = () => {
  console.log("Main");
  const manager = useRef<ValueType>({ dispatches: {}, values: {} }).current;
  const { dispatches, values } = manager;
  return (
    <>
      <button
        onClick={() => {
          values["A"] = (values["A"] || 0) + 1;
          dispatches["A"].forEach((v) => v({}));
        }}
      >
        A
      </button>
      <button
        onClick={() => {
          values["B"] = (values["B"] || 0) + 1;
          dispatches["B"].forEach((v) => v({}));
        }}
      >
        B
      </button>
      <button
        onClick={() => {
          values["C"] = (values["C"] || 0) + 1;
          dispatches["C"].forEach((v) => v({}));
        }}
      >
        C
      </button>
      <context.Provider value={manager}>
        <Component name="A" />
        <Component name="B" />
        <Component name="C" />
        <Component name="A" />
        <Component name="B" />
        <Component name="C" />
      </context.Provider>
    </>
  );
};

export default Page;

  • A を押した場合に発生する出力結果
A
A

まとめ

ContextAPI は Provider 内でデータを配るための機能です。一部で変な誤解が生み出されていますが、けっして無駄な再レンダリングを発生させる機能ではありません。特定のコンポーネントに対して再レンダリングを促したい場合は、各コンポーネント内で setState しディスパッチャーを使います。また、setState は再レンダリングイベントを発生させるためのディスパッチャーの役割が主要機能であって、データを保存するのが主目的ではありません。データを保存したいのであれば useRef が正しい選択です。

今回、解説用にディスパッチャーの呼び出しを剥き出し書いていますが、きちんとラッピングしてやればもっと使いやすくなります。

React を使うとき重要なのはディスパッチャーを適切なタイミングで呼ぶことです。それさえ出来ていればデータをミュータブルで扱っても全く問題ありません。タイミングを適切に管理して、再レンダリングの滝修行をアプリケーションに組み込むのは回避してください。

GitHubで編集を提案

Discussion

Yuichiro TachibanaYuichiro Tachibana

「ContextAPI と setState は本来組み合わせてはいけない」という主張は過激すぎるし一般に成り立たないと思います。

ContextAPI は Provider 内でデータを配るための機能です。

setState は再レンダリングイベントを発生させるためのディスパッチャーの役割

は同意ですが(※)、
この辺に気をつけて使おう、以上。くらいではないでしょうか。


(※)

データを保存するのが主目的ではありません。データを保存したいのであれば useRef が正しい選択です。

には非同意です。データ保存と再レンダリング実行でどちらが主目的というものではないと思います。
データ更新に伴って再レンダリングを実行するモデルこそがReactの根幹であって、そこは切り分けられないでしょう。むしろ副作用を伴う操作やそれに連動したデータ保存がReactのピュアな世界からの逸脱で、それらのためにuseEffectやuseRefが追加で用意されているくらいに考えます。


例えば、トップレベルでログイン中のユーザ情報をAPIから取得して、それをいろいろな子コンポーネントで使うのはよくあるケースだと思います。ContextAPIの出番だと思いますが、↓のようになって自然とuseStateとContextAPIを組み合わせることになると思います。

import React, { useState, useEffect, useContext } from "react"
import api from "api"

interface User {
  name: string;
  email: string;
}

const userContext = React.createContext<User | undefined>(undefined)

const useUser = (): User => {
  return useContext(userContext);
}

interface UserProviderProps {
  children: React.ReactNode;
}
const UserProvider = (props: UserProviderProps) => {
  const [user, setUser] = useState<User>()

  const { loggedIn } = useAuth()  // Pseudo code
  useEffect(() => {
    api.getUserMetadata().then((data) => setUser(data.user));  // Pseudo code
  }, [loggedIn])

  return (
    <userContext.Provider value={user}>{props.children}</userContext.Provider>
  )
}

// Application code
const Header = () => {
  const user = useUser()

  return (
    <div>{user ? user.name : "Logged out"}</div>
  )
}

const ComponentA = () => {
  const user = useUser()

  if (user == null) {
    return <div>Logged out</div>
  }

  return (
    <div>Your email is {user.email}</div>
  )
}

const App = () => {
  return (
    <UserProvider>
      <Header />
      <ComponentA />
      ...
    </UserProvider>
  )
}

(例なのでnullチェックなどは一部省いています)

ここでuserの保持にuseRefを使おうとは思いません。
userが更新されたのなら、それをsubscribeしている(useUser()を呼んでいる)コンポーネントは全て再レンダリングされて然るべきです。そのデータを元にviewを生成していて、元になったデータが変わったのですから。データからviewを生成するというReactのアプローチに至極忠実です。


翻って、本稿の例はContextAPIとsetStateの組み合わせで再レンダリングを「無駄に」引き起こす恣意的な例になっているだけという印象です。
親が管理するのが1個の大きなobjectで、子がそれをsubscribeして一部フィールドだけ使うというデザインそのものが、ContextAPIと相性が悪い(ように見える)というだけではないでしょうか。
1個のstateを管理する際の関心がContextAPIによって広範囲にばら撒かれてしまっているだけ、というか…
処方箋はuseStateとContextAPIを一緒に使わないことではなく、objectを使ったstate管理を考え直すことだと思います。よくあるのは更新される子要素ごとにstateを分けることですし、どうしても可変個が必要なら子コンポーネントの方でメモ化を使って重い計算や孫の再レンダリングを抑制する方向にいくとか。

空雲空雲

提示されたプログラムはUserProvider以下で、ユーザデータの更新と共にuseUserを呼んでいないコンポーネントも巻き込んで再レンダリングを引き起こしているようです。

記事の書き方が不十分で分かりにくかったことは申し訳ありませんが、そのコードはアンチパターンのようです。

Yuichiro TachibanaYuichiro Tachibana

わざわざ試していただいでありがとうございます(私の方でちゃんと実行しておらず不完全なコードでお手数おかけしたと思います。すみません)

私の方でもちょっと手直ししてやってみました。
こんな感じで useUser() を呼ばない <ComponentB /> があったときに、<UserProvider />内でsetUser()した際に<ComponentB />が再レンダリングされる、ということでしょうか。

// Application code
const Header = () => {
  const user = useUser();
  console.log("Header is rendered");

  return <div>{user ? user.name : "Logged out"}</div>;
};

const ComponentA = () => {
  const user = useUser();
  console.log("ComponentA is rendered");

  if (user == null) {
    return <div>Logged out</div>;
  }

  return <div>Your email is {user.email}</div>;
};

const ComponentB = () => {
  console.log("ComponentB is rendered.");
  return <div>Some contents</div>;
};

const App = () => {
  return (
    <UserProvider>
      <Header />
      <ComponentA />
      <ComponentB />
      ...
    </UserProvider>
  );
};

私の環境では再現しませんでした🤔

ただ、<UserProvider />の子供が再レンダリングされたとして、それは親がレンダリングされたからそのrender関数の中が一緒に実行されただけなのでは、という気もしています。
例がconfusingですみませんでした。


一方で私の主張はやはり変わりませんで、useStateによるステート管理&それに結びついたレンダリングのトリガーはReactの根幹なので無闇に忌避するべきではなく、むしろ極力Reactのライフサイクルに乗っておくべきと思っています。useRefはReactのライフサイクルを無視する特段の事情(RealDOMを触るとか、意図してメモ化を無視するとか)で使うべき例外です。

そもそも子コンポーネントの再レンダリングが起きること自体はアンチパターンというほど強い言葉で避けるものではないのではないでしょうか。子コンポーネントの計算量が増えるとアンチパターン"度"が上がるというような程度問題と思っています。
Reactの関数型的な(🪓が怖い…)実行モデルにおいては、計算量を無視できる理想的な世界では毎回全VDOMツリーをレンダリングする気持ちでコードを書き、しかし現実世界で計算量が問題になるからメモ化を駆使したりオブジェクトのmutabilityを気にしたりして最適化するんだと思っています。
計算量が重いコンポーネントに最適化が効いていなかったらそれは「アンチパターン」ですが、軽量コンポーネントならまあいいかなという。

と考えると、もし<UserProvider />の子供が再レンダリングされたとして、それ自体は(useContextのせいではなく親のrenderingのせいだと思うので)ContextAPIと結びつけて問題とすることではないと思いますし、
またそこでパフォーマンスが問題になるのであれば子コンポーネントをメモ化することで対処すべきものと思います。

空雲空雲

記事が分かりにくく誤解を生んで申し訳ありませんが、ContextAPIは全く悪くありません。
上位コンポーネントでuseStateを使うことによって、子コンポーネント全てに再レンダリングが発生する書き方をアンチパターンとしています。

ただ、<UserProvider />の子供が再レンダリングされたとして、それは親がレンダリングされたからそのrender関数の中が一緒に実行されただけなのでは、という気もしています

正にこれがアンチパターンです。
親の再レンダリングを行わず、子にデータを配るのが本記事の主題です。

再レンダリングを回避するディスパッチャーの呼び方は提示しておりますので、稚拙な記事ながらもう一度読んでいただければ幸いです。

Yuichiro TachibanaYuichiro Tachibana

親の再レンダリングを行わず、子にデータを配るのが本記事の主題です。

ふーむ記事の趣旨がちょっと分かったかもしれません。
勝手に私の言葉で言い換えます。間違っていたらご指摘ください。

  • 親に置いたuseRefで作ったrefに、複数の子からアクセスされる値を置くという設計を前提にする。
  • その値は、refで管理され、さらにmutable objectなので、fieldの値の変更はReactライフサイクルの埒外にある。従ってそのobjectをProvider.valueに渡しても、fieldの値の変更でProviderのrenderingが発火しない
  • 子には useState が置いてあって、そのdispatcherを呼ぶと同時に↑の値をmutateすることで、子だけrenderして親はrenderされない、ということができる

そして以上が"データを Context 側に持たせつつ、無駄な再レンダリングを防ぐパターン"の章のエッセンスであり、本記事で提供したいテクニックである。

いかがでしょう?


ContextAPIは全く悪くありません

ちなみに私も空雲さんがContextAPIそのものを悪いと言っているとは思っておりません。冗長な文章でわかりづらくすみませんでした。
「ContextAPI と setState は本来組み合わせてはいけない」(第1章タイトル)は偽ではないでしょうか、という意図です。

空雲空雲

いかがでしょう?

はい、その通りです

「ContextAPI と setState は本来組み合わせてはいけない」

useStateが正しいのですが、ここは後ほど修正します

「ContextAPI と setState は本来組み合わせてはいけない」(第1章タイトル)は偽ではないでしょうか、という意図です

「useStateを上位コンポーネントに持ってきてはいけない」が正確ですが、「ContextAPI と useState は本来組み合わせてはいけない」も特に偽ではありません。
組み合わせた結果、ご提示いただいたようなプログラムのように、データがナイアガラの滝のようなってアンチパターン化してしまうからです。

HIMURA TomohikoHIMURA Tomohiko

アンチパターンというほどでもなく、もっとパフォーマンス的に有利な方法があって、状態管理ライブラリは同様のことをしている、という話だと思いました。

子コンポーネントの状態をrefで親に制御を渡せるけど、それを祖先に持ち上げるのをcontextでバケツリレーをスキップできる、と言い換えてもよさそうですね。

Yuichiro TachibanaYuichiro Tachibana

はい、その通りです

どうもありがとうございます。

であれば、このテクニックが動作すること自体は理解しますが、トリッキーでReactの思想に沿っていないと感じます。

  • 「親のrefにmutable objectを置く。子はそれを参照・mutateしつつ自分で空のdispatchを行い自身のrenderingをトリガーする」というデザインを積極的に擁護できるユースケースが見出せません。
    • 複数の子で共有する値だから親に置きたい、というのであれば、なおさらReactのライフサイクルに則り親から子のrenderingをトリガーするべきです。Reactドキュメントが1章を割いてLifting State Upを説明している理由です。Reactは子componentが値を参照しているという知識に基づいて適切にrenderingを管理します。仮に不要なrenderingを抑制したいのであれば、それはstateの分割や子孫のメモ化が第一選択の解決策となります。
  • 逆に、ContextAPIとuseStateを組み合わせて使って、Reactのライフサイクルに乗っ取るとシンプルなAPIで書けることがほとんどである(個人的な経験からは100%ですが)と思います。
    • この際に起きるパフォーマンス問題は、Reactのライフサイクルから逸脱せずにメモ化で解決すべきだし、それで足ります

組み合わせた結果、ご提示いただいたようなプログラムのように、データがナイアガラの滝のようなってアンチパターン化してしまうからです。

これはuseContextを呼ぶ子全てにrenderingが発火することを指しているのでしょうか。
それはReactのデザイン上正しいものです。私の理解では「アンチパターン」とは呼べません。
それでパフォーマンス問題が起きるなら、先述の通り、state分割や孫のメモ化で解決できます。
逆にこれを肯定すると、「親のstateを子にprops, contextで渡す」というReactの根幹を成すデータフローが否定されます。

その前のコメントでも

正にこれがアンチパターンです。

とありましたが、私にとってこれは「アンチパターン」ではないので、「アンチパターン」という言葉の定義というか、この語から受ける印象がすれ違っているのでしょうか…
私はアンチパターン = ほぼ全てのケースにおいて忌避すべき書き方、くらいの強いネガティブな意味と捉えていました。
「親の再レンダリングを行わず、子にデータを配る」ことが主題である本記事においては、useContextでstateを参照している子の再レンダリングを発火するデザインを特にアンチパターンと呼ぶ、ということでしたら、なるほど限定的な意味として了解しました。

その上で、私は一般的な意味での「アンチパターン」という語のかなりネガティブな印象に基づいて、
任意の読者がこの記事を読んだ際に「ContextAPIとuseStateを一緒に使うのは基本的にダメなことなんだ」「データを保存する時は基本的にuseStateよりuseRefを使うべきなんだ」と理解するのではないかと思い、
それは違うよと言いたくてコメントしました。
(「アンチパターン」という単語の受け取り方にかかわらず、第1章タイトル「ContextAPI と useState は本来組み合わせてはいけない」や、まとめの「データを保存したいのであれば useRef が正しい選択です。」を見たら大多数の方はそう理解するのではないかと思いますが)

記事の主題ではないところに噛み付かれているとお感じかもしれませんが、
かなり強い表現で断定調で書かれている文章でしたので反応せずにはおれませんでした。

一応動作するテクニックとして"mutable objectをrefでContext 側に持たせつつ、無駄な再レンダリングを防ぐパターン"を紹介する内容であれば、確かにそれでも動きますねでいいのですが、
ほぼ全ての(私には例外が思いつきませんが)ケースで従うべきであろうReactの思想に則ったデザインを「アンチパターン」呼ばわりする記述は悪影響が大きいと思います。
最初の章のタイトルである「ContextAPI と useState(setState)は本来組み合わせてはいけない」も、一般に成り立ちません。むしろuseRefなどを使って純粋関数の世界を破壊するデザインの方が例外でしょう。

空雲空雲

これはuseContextを呼ぶ子全てにrenderingが発火することを指しているのでしょうか。

これは違います。
useStateを持っているコンポーネント以下のツリー全てが再レンダリングされることです。
記事がContextAPIの話と混ざってしまって申し訳ありませんが、useContextの効果とは関係ありません。
useStateのデータをContextに持たせることで、useContextとは関係なくデータの書き換えがツリー全体の再レンダリングの引き金になっていることがアンチパターンなのです。

以下、サンプルを作りましたので確認ください。
分かりやすいようにuseContextを持たないComponent2を追加しています

HIMURA TomohikoHIMURA Tomohiko

横槍失礼しますが、

useStateのデータをContextに持たせることで、useContextとは関係なくデータの書き換えがツリー全体の再レンダリングの引き金になっていることがアンチパターンなのです。

例において、Dという出力がボタンを押すたびに出力されるという意味であれば、Providerの子要素を再生成しているのが原因なので、これを抑制すれば起きないとおもいます。
useMemoで一度しか生成されないようにしたり、Pageコンポーネントのpropsで受け取ったりする手があります。

Yuichiro TachibanaYuichiro Tachibana

サンプルありがとうございます。

useContextとは関係なくデータの書き換えがツリー全体の再レンダリングの引き金になっていることがアンチパターンなのです。

なるほどそちらでしたか。

しかしそれに関しても私の主張は同じで、

  1. stateの更新が起きたcomponentの再レンダリング発火はReactのデザインとして正しい
  2. 再レンダリングが起きたcomponentの子の再レンダリング発火もReactのデザインとして正しい

従って問題ではない(アンチパターンではない)、
です。

これらはどれもReactの思想として正しいはずです。
それが現実にはしばしばパフォーマンス問題を引き起こしますが(それを問題視しておられると思うのですが)、それに対してReactは React.memo() を解決策として提供しています。
今回の例でも Component2 をメモ化すれば Component2 の再レンダリングは起きません。

const Component2 = React.memo(({ name }: { name: string }) => {
  console.log(name);
  return <div>{name}</div>;
});

親に ref を置くことも解決策かもしれませんが、Reactの設計者はそのようなコードを意図していないだろう、ということです。
本記事のコードは、Reactの世界の中に無理やり治外法権のstate objectを置いていて違和感があると言っても良いです(そういう意味では Tomohiko Himura氏の「もっとパフォーマンス的に有利な方法があって、状態管理ライブラリは同様のことをしている」はその通りだと思っていて、必要な場面で治外法権なstate objectを持つのはありえます。ただ、それを以て、普通のやり方を「アンチパターン」呼ばわりするのは違うだろう、と思います)


また、上の私の例では <UserProvider /> が切り出されているので、<UserProvider />の再レンダリングは子に伝播しないのではないでしょうか(上述の通り、私の手元ではおっしゃる問題は再現しませんでした)。
この挙動は非自明ですが、Reactは頭がいいので最適化してくれているのでしょう。もしそうでなくても、同様に子を React.memo するのがReact wayだと思います。

空雲空雲

上の私の例では <UserProvider /> が切り出されているので、<UserProvider />の再レンダリングは子に伝播しないのではないでしょうか

確かにUserProvider以下では{children}が変化しないため、useContextをもつコンポーネント以外は再レンダリングされませんでした。そのため限られたデータに対してContextをuseStateで使う場合はアンチパターンではなく、Yuichiro Tachibanaさんのプログラムはアンチパターンではありませんでした。お詫びいたします。

Tomohiko Himuraさんもサンプルありがとうございます。

stateの更新が起きたcomponentの再レンダリング発火はReactのデザインとして正しい
再レンダリングが起きたcomponentの子の再レンダリング発火もReactのデザインとして正しい

上記はReactの動作として正しいです。しかし持たなくて良い場所でステートを持つこととは関係しません。
必要の無い再レンダリングが起こらなければ、わざわざメモ化する必要もないからです。

ただ、それを以て、普通のやり方を「アンチパターン」呼ばわりするのは違うと思います)

前述でおっしゃるとおり、メジャーどころの状態管理ライブラリは治外法権のstate objectを持ってくるのが普通なので、普通のやり方というなら現在ではこちらの方が普通です。時代と共に比較上効率が悪いものがアンチパターンになってしまうのは仕方がありません。

Yuichiro TachibanaYuichiro Tachibana

しかし持たなくて良い場所でステートを持つこととは関係しません。

売り言葉に買い言葉になってしまいますが、むしろrefの例こそ「使わなくて良い場所でrefを使っている」と私には思えます。
私にとっては、useStateを使った例は「Reactの流儀に従い自然な場所でステートを持っている」です。その結果起きうる性能問題はReact側も認識していて、公式にReact.memoという解決策を提供しています。そこまで含めて、React流です。

必要の無い再レンダリングが起こらなければ、わざわざメモ化する必要もないからです。

これも私に言わせれば、「メモ化しておけばわざわざ不自然なrefを使う必要はない」です。

メジャーどころの状態管理ライブラリは...

うーん、やはり「普通」「アンチパターン」という言葉の使い方に齟齬があるんですかね…
私にとってはuseStateを使うやり方は普通ですし、状態管理ライブラリ使うのも普通です。要件に応じて使い分けるものです。ちなみに適切なライブラリを使わずに自前で変なrefを使うのが私にとっては一番アンチパターン度が高いです。
また効率悪いと言ってもごく微々たるものです。Reactのクリーンなデザインを破壊する理由にするほどのものではありません。useStateの例がアンチパターンなら、React公式がuseStateをdeprecatedに指定するとか何かドキュメントに書くとかするはずですよね…


React を使うとき重要なのはディスパッチャーを適切なタイミングで呼ぶことです。それさえ出来ていればデータをミュータブルで扱っても全く問題ありません。タイミングを適切に管理して、再レンダリングの滝修行をアプリケーションに組み込むのは回避してください。

もそれ自体は真だと思うのですが、通常のuseStateによるステート管理をアンチパターンと切り捨てるほどの強い解決策ではない、というのが私の主張です。
「それさえ出来ていれば」は言い換えれば「それをやる責任を開発者が意図的に負う」ということです。その獣道に進むことは状況によっては有効かもしれませんが、公式が意図した通りの舗装された道を歩く選択をアンチパターン呼ばわりしないで欲しいということです。後者は性能上の微々たる不利があるかもしれませんが、Reactのライフサイクル管理に乗っかることで、ほとんどの状況でその不利を補って余りあるクリーンなコード、ひいては安全性をもたらします。決して「アンチパターン」などという強い言葉で下に見れるものではないはずです。「必要に応じて最適化の余地がある」くらいが誠実ではないでしょうか。


そのため限られたデータに対してContextをuseStateで使う場合はアンチパターンではなく、Yuichiro Tachibanaさんのプログラムはアンチパターンではありませんでした。

こちら確認いただきありがとうございます。
この文を見て思ったのですが、ということはここでいう「アンチパターン」というのは、個別のプログラムのrendering状況をinspectorで見るなりconsole.logでチェックするなりして、不要な再レンダリングが起きていたらアンチパターン、そうでなければOK、みたいな意味なのでしょうか。

空雲空雲

むしろrefの例こそ「使わなくて良い場所でrefを使っている」と私には思えます。

例えばReduxのhooks機能に関して、内部でContextAPIを使いつつuseStateを使わない組み合わせでStoreデータの管理を行っています。ReduxがuseStateを使っていないことが不自然でしょうか?

公式にReact.memoという解決策を提供しています。そこまで含めて、React流です。

React流という用語の定義はよく分かりませんが、例えばReduxで状態管理を行った場合、再レンダリングの抑制にメモ化の必要はありません。それはReact流ではないということでしょうか?

ContextAPIにuseStateのデータを設定することで、パフォーマンスが落ちたり冗長な記述が必要になるのはアンチパターンです。そのためメジャーどころの状態管理ライブラリはuseStateのデータをStoreに使いません。

公式が意図した通りの舗装された道を歩く選択をアンチパターン呼ばわりしないで欲しいということです。

ContextAPIとuseStateの組み合わせはReact公式のリファレンスやチュートリアルに存在せず、公式の方法ではありません。

不要な再レンダリングが起きていたらアンチパターン、そうでなければOK、みたいな意味なのでしょうか。

はい、その通りです。

どんなに冗長に書いても、小規模なプログラムなら目立たないので問題は発生しにくいです。しかしContextAPIとuseStateの組み合わせはプログラムは規模が大きくなるにつれて、パフォーマンスの劣化とそれに対応するためもメモ化やContextの分離など、本来必要の無いクリーンではないコードの増加を生みます。それがアンチパターンの結果です。

Yuichiro TachibanaYuichiro Tachibana

Reduxはまさに良い例ですね。
Redux自体はReactと関係ないデータストアです。それがreact-reduxでReactに接続する際にrefとかが登場したり自前でdispatcherを呼んだりします。React管理下の世界とその外との境界を跨ぐ事になる(副作用やレンダリングタイミングに意識的になる)ので。

ReduxがuseStateを使っていないことが不自然でしょうか?

なので、Redux(や react-redux)がuseStateを使っていないことは自然です。React管理下の世界の外側にstoreオブジェクトがあるので。

その他上記のコメント全体を通して、問題意識がわかった気がします。
Context分割もメモ化もしたくない、けど再レンダリングは抑制したいんですね。
であればそのような状態管理ライブラリを使うのが解であると主張をされるのではダメでしょうか。それは否定しません。
そして空雲さんは嫌いなようですが、ContextAPIとuseStateだって、適切な分割とメモ化と共に使ってレンダリング抑制の解になります。この辺の前提を飛ばして「ContextAPI と useState は本来組み合わせてはいけない」と主張するのは乱暴だと言っています。

また一方で、そういう状態管理ライブラリを使わずに、Reactを使ったアプリケーションコードの中に小さなredux + react-reduxを再実装するような書き方は一般に勧められないでしょう。それこそアンチパターンと言って良いと思います。
本文にもありますが「構造が少々複雑になります」よね。バグのもとです(空雲さん個人はこれでバグを生まないのかもしれませんが、一般論です)。
ちなみに私も知らなかったのですが、加えて https://qiita.com/uhyo/items/6a3b14950c1ef6974024 こんな問題もあるようです。
Reactが隠蔽してくれていたことを開発者側の責任に引き寄せた=React流(下記参照)から逸脱する弊害です。
Reduxなどのちゃんとメンテナンスされたライブラリを使うなら、アプリケーション開発者にはこれらのデメリットが見えないので特に問題は感じません。

…しかし空雲さんのプロフィールを見ると「車輪の再発明が生業」なんですね。仮に今回の例をそのレベルで肯定されるならもう何も言えません。


React流という用語の定義

すみません勝手に言葉を作りました。Reactの思想に沿った書き方・Reactが隠蔽してくれている機能には素直に乗っておく書き方、くらいの意味でした。

例えばReduxで状態管理を行った場合、再レンダリングの抑制にメモ化の必要はありません。それはReact流ではないということでしょうか?

はい。ライブラリ内部はステート管理に関してReact流でないと言えるでしょう。理由は上述の通りです。しかしその部分は隠蔽されているので利用者からは気になりません。

そのためメジャーどころの状態管理ライブラリはuseStateのデータをStoreに使いません。

はい。素直にそれらのライブラリを使うのがいいと思います。

メモ化やContextの分離など、本来必要の無いクリーンではないコード

私はもう慣れましたが、これが気持ち悪い(Reactはなぜこの問題が解決できないんだ)という気持ちも分からなくはないですが…
とは言っても本記事の例だとContextの分割やメモ化を不要と言って避ける代わりに、本来必要でないrefを使い、開発者に余計な責任が生まれ、複雑な構造が現れますよね。
このトレードオフは無視できません。


ContextAPIとuseStateの組み合わせはReact公式のリファレンスやチュートリアルに存在せず、公式の方法ではありません。

分かりづらくてすみませんでしたが、ここは本文の

「setState は(…中略…)データを保存するのが主目的ではありません。データを保存したいのであれば useRef が正しい選択です。

なども反論の射程に含んだつもりでした。公式が useState でデータ保存してるのだから、useRefがデータ保存のための正しい選択ではないし、それを基ににアンチパターンを語るならそれは違うだろうと。

ContextAPIとuseStateの組み合わせに関しては、確かにドンピシャなのは公式チュートリアルにないですね。ここはそのように設計して上手くいっている個人的な経験がベースになります。私はContext分割もメモ化も抵抗ありません。


はい、その通りです。

なるほど。
大規模なReactアプリでパフォーマンス問題が起きうることには同意します。
それをアンチパターンと呼ぶかどうかで私は感覚が違うんだなと思いました。
私にとってこれは大規模化した際に顕在化するパフォーマンス問題で、あくまで程度問題です。最適化の対象にはなりえますが、アンチパターンとまで呼ぶのは抵抗があります。

むしろ「XXの書き方は基本的にダメだ」とか「YYなデザインは基本的にダメだ」が私にとっての「アンチパターン」です。
↓のような文章はまさにそんな感じですので、これらに反応してしまいます。

「ContextAPI と useState は本来組み合わせてはいけない」

「setState は(…中略…)データを保存するのが主目的ではありません。データを保存したいのであれば useRef が正しい選択です。

空雲さんとしては再レンダリング抑制が記事の主題なのにこれらの文章に噛みつかれて不思議かもしれませんが、
少なくとも私には、これらは再レンダリング抑制の文脈に関係なく、一般に忌避すべきパターンとして紹介されているように読めます。そしてそれは違うと反論した次第です。
そして私はコメント2往復目でやっと主題を読み取りました。

空雲空雲
  • ContextAPIを使っているReduxの実装方法は良くて、ContextAPIを使っている今回のプログラムは不自然
  • メモ化やContextの分割など余計なコードが発生してもプログラムが冗長化しても、React流で抵抗感がないのでアンチパターンではない
  • コードの冗長化抑制やパフォーマンスではなく、不自然、抵抗感など心の持ち方の話が重要

パフォーマンス的に劣るとか言う話ならともかく、「お気持ち」の部分は反論しようがありません。
React流という定義も公式が推奨しているものではなく、「お気持ち」や自分がそう感じたものになってしまっています。

なども反論の射程に含んだつもりでした。公式が useState でデータ保存してるのだから、useRefがデータ保存のための正しい選択ではないし、それを基ににアンチパターンを語るならそれは違うだろうと。

ちなみにreact-reduxはuseRefで管理用のオブジェクトを作成して、それらをuseMemoでひとまとめにする実装を行っています。
正しい選択ではないという主張なら、Reduxの正しい実装方法をお教えいただければありがたいです。

Yuichiro TachibanaYuichiro Tachibana

一部お気持ちな表現を含めてしまったのは事実ですが、ちゃんとそのお気持ちに至る具体的な理由や、それ以外の合理的な反論やその理由も書いてあるのでそこを無視しないでいただけますかね…
頂いた反論に対する再反論も全て1個前のコメントに既に書いてある内容で事足ります。

React流という定義も公式が推奨しているものではなく

これもまあこの言葉自体はそうなんですが、その私なりの意味について「Reactの思想に沿った書き方・Reactが隠蔽してくれている機能には素直に乗っておく書き方」と書きました。それに沿わない場合の具体的な弊害もちゃんと書きました。
それでもこの書き方がそうでない書き方に劣後するというなら、もうプログラマとして信仰している宗教が違うんだろうなと思います。

空雲空雲

useRefがデータ保存のための正しい選択ではない

に対するReduxの正しい実装方法だけは、お教えいただきたいです。
広く使われているものが正しいとは限らないことは当然あるわけで、他に最善の方法があるなら是非知りたいです。
react-reduxは何を使ってデータ保存すれば良いのでしょうか?

Yuichiro TachibanaYuichiro Tachibana

Reduxの実装方法に問題はないと思っています。上の私のコメントでもReduxやその他の状態管理ライブラリを批判する記述ないはずです。むしろ「Redux(や react-redux)がuseStateを使っていないことは自然です。」などで肯定しています。
上にも書いた通り、Redux自体はReactとは独立したステート管理ライブラリです。従ってReduxのステート管理機構の実装にReactの知識は入り込みません(reduxのコードに React は登場しません)。

それがReactと接続する際の界面でrefやらdispatcherやらを意識的に使うことで、うまくReactから使えるようになっています(react-redux の部分)。ここはreact-redux開発者の責任において、Reactのライフサイクル管理と整合するように注意深く実装されている部分です。

私が問題視しているのは

  • それらのライブラリを使わずに、アプリケーションコード内に似た仕組みを再実装すること
  • ライブラリを使わない生のReactのステート管理機構が、そのような再実装に劣後するという考え方、あまつさえアンチパターンとまで言われてしまうこと。またそれに関連する記述(具体例は上のコメント参照)。

の2点です。
理由は上のコメントをご覧ください。

空雲空雲

ライブラリを使わない生のReactのステート管理機構が、そのような再実装に劣後するという考え方、あまつさえアンチパターンとまで言われてしまうこと

Reactのステート管理機構は問題ありません。ご自分がいつも使っている方法を公式の方法と捉えていませんか?今回のような内容が先に流行っていたら、なんの疑問も持たずに受け入れていたのではありませんか?

前述したとおり、React公式にはuseStateのデータをContextAPIに入力する方法はありません。Reduxも今回作成したプログラムも「Reactのステート管理機構」の中での実装です。特別なことをしているわけではありません。再実装ではなくReactが用意している標準機能を使っているだけです。

それらのライブラリを使わずに、アプリケーションコード内に似た仕組みを再実装すること

もちろん必要ならライブラリを使うべきです。
今回はContextAPIを直接使う内容なので、アンチパターンを生まない方法を「Reactのステート管理機構」の範囲で記事にしました。

なので出来ればご自分の中での標準ではないという主張では無く、パフォーマンスやコードの冗長化、こういう問題が起こるから使い物にならないなど、実際に起こりうる話をしていただけるとありがたいです。

たとえば以下のような内容です。

https://qiita.com/uhyo/items/6a3b14950c1ef6974024

時代と共にReduxの実装ですら問題が起こりうるので、こういう内容なら新たな対処法や作り方が模索できます。

Yuichiro TachibanaYuichiro Tachibana

Reactのステート管理機構は問題ありません。

えぇ…

setState は(…中略…)データを保存するのが主目的ではありません。データを保存したいのであれば useRef が正しい選択です。

って本文で言ってるじゃないですか…

"Reactのステート管理機構"が指す対象が不明瞭でしたかね? useState のことです。
useRef は"Reactのステート管理機構"ではありません。これはReactのライフサイクル管理の世界の外側を触るための副作用を伴うエスケープハッチです。react-reduxは、"Reduxというステート管理機構(Reactのステート管理機構ではない)"をReactにrefやらdispatcherやらで接続しているものです。


なので出来ればご自分の中での標準ではないという主張では無く、パフォーマンスやコードの冗長化、こういう問題が起こるから使い物にならないなど、実際に起こりうる話をしていただけるとありがたいです。

これも上のコメントで既に述べています。以下に再掲します。読めていないんですかね…?

本文にもありますが「構造が少々複雑になります」よね。バグのもとです(空雲さん個人はこれでバグを生まないのかもしれませんが、一般論です)。
ちなみに私も知らなかったのですが、加えて https://qiita.com/uhyo/items/6a3b14950c1ef6974024 こんな問題もあるようです。
Reactが隠蔽してくれていたことを開発者側の責任に引き寄せた=React流(下記参照)から逸脱する弊害です。

(…略…)本記事の例だとContextの分割やメモ化を不要と言って避ける代わりに、本来必要でないrefを使い、開発者に余計な責任が生まれ、複雑な構造が現れますよね。
このトレードオフは無視できません。

Yuichiro TachibanaYuichiro Tachibana

もう売り言葉に買い言葉で返しますが、

ご自分がいつも使っている方法を公式の方法と捉えていませんか?

そのままお返しします。

今回のような内容が先に流行っていたら、なんの疑問も持たずに受け入れていたのではありませんか?

今回のような内容がなぜ流行っていないかを考えてください。というか私のここまでのコメントはその理由の説明になっています。

Yuichiro TachibanaYuichiro Tachibana

新たな対処法や作り方が模索できます。

その姿勢は基本的に素晴らしいと思いますが、こと本記事の内容に関しては

  • ReactのAPIに従っていればそんな苦労はいらない
  • react-reduxとかのライブラリはメンテナがその苦労を背負ってくれているので、ライブラリも利用者でいる限りそんな苦労はしなくて済む
  • 空雲さんが自前で実装してその苦労を背負うのは構わないが、本記事の内容だと、その苦労を背負う覚悟のない読者に知らず知らずのうちに苦労を強いることになるので悪影響があると思う。
    • そんな苦労を背負いたくない読者が大半であろうし、ならば彼らに勧めるべきはReactのAPIに従った書き方だろう。それをアンチパターンと呼ぶ点も害である。
    • むしろライブラリメンテナでもないのにそんな苦労を背負うリスクを取ることこそ、一般的にアンチパターンと言われるだろう。

ということです。

空雲空雲

setState は(…中略…)データを保存するのが主目的ではありません。データを保存したいのであれば useRef が正しい選択です。

って本文で言ってるじゃないですか…

はい、言っています。パフォーマンス上、ステートを保存するのはuseStateを使い、データを保存するのにはuseRefが正しい選択です。そして今回のプログラムは、ステートの変化を通知するためにコンポーネント内のuseStateを呼び出しています。これは"Reactのステート管理機構"を使って呼び出しています。"Reactのステート管理機構"に問題があったら出来ない動作です。

そんな苦労を背負いたくない読者が大半であろうし、ならば彼らに勧めるべきはReactのAPIに従った書き方だろう。それをアンチパターンと呼ぶ点も害である。

大半の読者のお気持ちはその読者にしか分かりません。しかし自分の意見を通すため、調査をしたわけでもない他人の気持ちを勝手に決めつけて、自分の論拠の付け足しにするのはいかがなものでしょうか?

ReactのAPIに沿った書き方というのがReact公式には掲載されておりません。もしそれらのドキュメントがあるなら提示をお願いします。

(…略…)本記事の例だとContextの分割やメモ化を不要と言って避ける代わりに、本来必要でないrefを使い、開発者に余計な責任が生まれ、複雑な構造が現れますよね。

パフォーマンス上、Contextに格納するデータとしてrefは必要であり(ReduxはuseMemoでとりまとめている)でuseStateは不要です。本来とかAPIに従うというのが度々出てきますが、どれも公式には定められておりません。

また、今回の内容はそれほど複雑な構造はしていませんが、具体的に間違えやすいポイントなどを示していただければと思います。ぜひ、解説記事として取り上げさせてください。

Yuichiro TachibanaYuichiro Tachibana

大半の読者のお気持ちはその読者にしか分かりません。

はい、ここにすぐに出せる客観的な裏付けはないです。
私は同じ考え方をするプログラマが相当数いることを信じてこう書きましたが、この議論の参加者間でその点に合意が取れないなら、それに基づいた主張はここで終わりにします。
このコメントを見た第三者の判断材料として残すに留めます。賛同してくれる方がいることを願うばかりです。
ただし当該文章以外の、他の箇条書き部分の主張が変わるものではありませんのでそちらは参照し続けていただき、できればご意見賜れますと。

ただ、ここを深掘りしだすと、
なるべくReactのライフサイクル管理に乗っかって楽をする派(私) vs パフォーマンスのためにはReactのライフサイクル管理に関わる部分に自分で触ること、その結果として表出する複雑性やメンテナンス性の低下を気にしない(複雑と思わない)派(空雲さん)
の価値観のぶつかり合いなんですかね。
この仮説は以下の議論でも補強されると思います。


ReactのAPIに沿った書き方というのがReact公式には掲載されておりません。もしそれらのドキュメントがあるなら提示をお願いします。

本来とかAPIに従うというのが度々出てきますが、どれも公式には定められておりません。

普通にReactのチュートリアルで紹介されている書き方のことですよ。せっかくReactが自動でやってくれていることには素直に乗っかろうという書き方です。Reactがconst [state, setState] = useState()というAPIを提供しているのだから、それに乗っかってstatesetStateは両方使って再レンダリングはReactに任せようという書き方です。

もしContextAPIとuseStateの組み合わせを特別に気にされているのであれば、上に書いた通り、それに関してはドンピシャなのは無いですが、
それを言い出したらデータ管理にrefを使うことを擁護する公式ドキュメントだって無いんじゃないですかね。

で、「データを更新したらviewがリアクティブに更新される」というReactの基本思想に忠実であろうとすれば自然とこうなりませんかね。Reactはその思想を擁護しつつ現実的なパフォーマンス問題に対処するためにメモ化の手段だって提供しています。

あと、これは補助的なものですが、"Don’t Overuse Refs"というメッセージもあります。これの本文の内容自体はこの議論と直接関連しないのですが、これもあり私はrefの濫用を避けよう(同じ問題を解決できるならrefを使わない策を選ぼう)という基本姿勢でいます。

そこでパフォーマンスを優先して、あえてReactがやっていくれている裏側の仕事にrefやらdispatcherやらで触ろうというのは、refやらレンダリングサイクルやらを意識できる上級者が思いつけるハックだと感じます。
上でも述べましたが、それが動くことは理解しますが、メリットとデメリットの天秤が釣り合っていないと感じます。
だって微々たるパフォーマンス改善のためにReactを使うことの恩恵を一つ手放しているんですよ?

(「メリットとデメリットの天秤が釣り合っていない」は私の「お気持ち」です。一方でここに合意できないならそれもまた「お気持ち」です。価値観のぶつかり合いといった所以です。「微々たる」とかも掘り下げれば詳しく説明できますが最終的にはお気持ちとか経験ベースの話に落ち着くんだと思います。)


はい、言っています。

あー、理解しました。その文章での"ステート"と"データ"はそういう使い分けでしたか…
では
そこについては対話を1往復巻き戻して、その前の私のコメントで書いた

ライブラリを使わない生のReactのステート管理機構が、そのような再実装に劣後するという考え方、あまつさえアンチパターンとまで言われてしまうこと

という文章を説明しなおすことで再反論とさせていただきます。
この文章は、まさに今回仰った

そして今回のプログラムは、ステートの変化を通知するためにコンポーネント内のuseStateを呼び出しています。

において、useStateは変化の通知だけに使い、データを別のところに保存している方法を「そのような再実装」と表現しています。
それと対比される"Reactのステート管理機構"は、本来useStateを使って呼び出す、ステートの保存・変更検知・componentの再レンダリングを自動で行う機構という意味です。
この二つを対比して、後者を劣後させることを問題にしています。


具体的に間違えやすいポイントなどを示していただければ

例えば子で"mutable objectへのmutation"と"dispatch"の2つを同時にしなければならない部分。useState使っていれば1回の関数呼び出しで終わるところ、裏側の知識が剥き出しになり関心ごとが増えてしまいます。componentが増えてきたらdispatch忘れとかありそうですよね。
それならカスタムフック作れば…となりますが、どんどんライブラリっぽくなってきましたね。じゃあ既存のよくできた状態管理ライブラリ使えば良くないですか。

空雲空雲

それを言い出したらデータ管理にrefを使うことを擁護する公式ドキュメントだって無いんじゃないですかね。

もちろんありません。「ReactのAPIに沿った書き方」という定義されていないことを標準のように語られていたのでそこを指摘させていただきました。

そこでパフォーマンスを優先して、あえてReactがやっていくれている裏側の仕事にrefやらdispatcherやらで触ろうというのは、refやらレンダリングサイクルやらを意識できる上級者が思いつけるハックだと感じます。

ContextAPIでuseState(useReducer)を使う場合、useContext以降でステートの変更を行うため、以下のような書き方を見かけます。Contextにdispatcherを含めて配ることによって、コンポーネント外からstateを変更するものですが、こちらの実装も駄目と言うことでしょうか?

const [state,setState] = useState();
return <Provider value={{state, setState}}>・・・</Provider>

において、useStateは変化の通知だけに使い、データを別のところに保存している方法を「そのような再実装」と表現しています。
それと対比される"Reactのステート管理機構"は、本来useStateを使って呼び出す、ステートの保存・変更検知・componentの再レンダリングを自動で行う機構という意味です。
この二つを対比して、後者を劣後させることを問題にしています。

別々に行う方法に不満があれば、記事中で紹介している

  • 無駄な再レンダリングを起こさないパターン

こちらの方は同じuseStateの[state,setState]の組み合わせで使用しています。通知だけに使用するやりかたも追加で紹介していますが、きちんと両方書いていますのでお読みくだされば幸いです。

また、続きとして書かせていただいた

https://zenn.dev/sora_kumo/articles/52ff6d925468e8

の方でも同様で、最終的にはコンポーネント内で作られたsetStateに値を設定し利用する構造です。

例えば子で"mutable objectへのmutation"と"dispatch"の2つを同時にしなければならない部分。useState使っていれば1回の関数呼び出しで終わるところ、裏側の知識が剥き出しになり関心ごとが増えてしまいます。componentが増えてきたらdispatch忘れとかありそうですよね。

はい、こちらも通知を別にする書き方の方のみに当てはまる内容です。useRefとContextAPIの組み合わせで必ず発生する問題ではありません。

それならカスタムフック作れば…となりますが、どんどんライブラリっぽくなってきましたね。じゃあ既存のよくできた状態管理ライブラリ使えば良くないですか。

はい、その通りです。

  • useStateとContextAPIの組み合わせだと、コードが大きくなるにつれてアンチパターン化する
  • ContextAPIでのパフォーマンスアップを実装する
  • よくよく考えたらライブラリを使った方が楽なので移行する

この流れは不自然では無いと考えます。

  • useStateとContextAPIを組み合わせるとアンチパターンになる
  • ContextAPIが悪いわけでは無く、その使い方が悪い

という話なので、状態管理ライブラリを採用するとっかかりになるのならそれで良いのではないでしょうか?

Yuichiro TachibanaYuichiro Tachibana

結構無視されてしまった部分があるので、まず私が核心だと思っているトピックを整理して再掲します。

まず、私は↓と思っています。

  1. 「stateを変更→関連するcomponentが再レンダリング」の自動化がReactの基本思想で、かつReactを使って得られる恩恵の一つ。なので極力それに従った書き方をすべき。恩恵 = 開発者が考えることが減る→構造の理解が楽になりバグも減る。
    1. それで起きる性能問題はメモ化で解決するべき。メモ化で増える記述量はReactを使う以上甘受すべきものだし、性能問題との天秤は十分釣り合っている。
    2. これがライブラリとしての基本思想なので、これから外れるとReact本体の変更に追従するのが大変になる可能性がある。それが表出した一つの例が https://qiita.com/uhyo/items/6a3b14950c1ef6974024
  2. 既存の状態管理ライブラリを利用するのも同様に有効な手段である
    1. ライブラリ内部は1.の考え方に沿っていないかもしれないが、それは隠蔽されているので利用者としては問題ない。
  3. 2.で1.に反したライブラリの実装を擁護したからといって、アプリケーションコードで1.に反した実装をすることは擁護しない
    1. ライブラリがOKなのは、その問題がライブラリ内部に隠蔽され、十分スキルのあるライブラリメンテナが対処してくれるから。これは一般に、自前での再実装よりライブラリ利用の方が推奨される理由でもある。
      • 従って一般の開発者に向けた記事で1.に反した実装を勧めるのは悪影響がある。

(補足): 1.と2.は状況・要件によって使い分けるもので一般化できる優劣はない。1.をアンチパターン呼ばわりするのはおかしい。

そして上のコメントで、3-1.について「勝手に大半の読者の気持ちを推測するな」と言われてしまいました。元々私は3-1はプログラマの一般的な価値観だと思っていて、ここは合意できると期待していたのですがダメでしたね。浮雲さんは車輪の再発明が生業だからでしょうか。ここがダメだと、この議論は「根本的なところで合意できないということ」に合意して終着な気がしています。

ちなみに3-1以外も、それぞれの理由をこれまで掘り下げてきたわけですが、そのまま行っても、どこか基本的に価値観のレベルで合意できないところに辿り着く気がしています。

ここまでが核心だと個人的には思っているのですがいかがでしょうか?

ちなみに「Reactの基本思想」みたいなまた嫌われそうな言葉を使ってしまいましたが、Why did we build React?の内容をもとに私はそう理解しています。

以下は直前のコメントに対する各論の返信です。


(…略…)以下のような書き方を見かけます。(…略…)こちらの実装も駄目と言うことでしょうか?

これはいいと思いますよ。
この state が書きかわったら、このProviderの下にいるConsumers (useContextしているコンポーネントたち)はReactが自動で再レンダリングしますよね。私の主張通りです。

より正確にいうと以下の流れですが。説明のために全体を SomeProvider で囲みます。

const SomeProvider = () => {
  const [state,setState] = useState();
  return <Provider value={{state, setState}}>・・・</Provider>
}
  1. state が変化
  2. SomeProvider が再レンダリングされる
  3. Providerのvalue({state, setState})が変化するので、Consumersが再レンダリングされる。

また、この書き方がOKなら、本文第1章タイトル「ContextAPI と useState は本来組み合わせてはいけない」はやはり間違い、 もしくは仮にそれ以外の意図があったとしてもミスリーディングではありますよね。

無駄な再レンダリングを起こさないパターン

これは

  • 子が statesetStateを持つ。
  • 子のsetStateをContext経由で親に上げ、親がそれを呼ぶ

ということですよね。
これはReactの教条主義的に反論するなら、子の操作を親に持ち上げている点がReactが推奨するパターンに反するので問題です。
図らずも前のコメントでリンクしたDon’t Overuse Refsはむしろこの論点に近いです。何か操作を行うためのAPIを子が外部に公開するより、親にstateを持たせてそれを下に降ろすことをまず考えてください。リンク先は、refがそのような用途に利用されがちなのでrefを対象にした文章ですが、原則は同じです。原則とはLifting State Upの内容です。

もし「それでは親の再レンダリングが起きる」という反論があれば、私は「それがReactの基本思想に従う書き方であり、メリットが最も大きいデザインなので問題ない」と再反論します。おそらくここまで何度もやってきた話ですし、本コメントの最初に書いた1.の意見の通りです。この点で合意に至れないならこれ以上の深掘りは無理かなと思います。

状態管理ライブラリを採用するとっかかりになるのならそれで良いのではないでしょうか?

はい、そのくらいの姿勢であれば賛同します。
本文には「状態管理ライブラリを採用するとっかかりとして読んでね」というガイドがなく、アプリケーション開発者が自分でこの書き方をするのを薦めるように読めたので問題だと思った次第です(上記論点3-1.)。


ちなみに、メモ化が面倒という点はReact側も認識していて、自動でメモ化してくれるコンパイラ(React Forget)を実験中らしいです。

React without memo (YouTube)

逆説的に、今現在は申し訳ないけど面倒でも手動でメモ化してくれ、というのがReact本体のデザインです。React Forgetがこの問題を解決してくれるといいですね。

空雲空雲

これがライブラリとしての基本思想なので、これから外れるとReact本体の変更に追従するのが大変になる可能性がある。それが表出した一つの例が https://qiita.com/uhyo/items/6a3b14950c1ef6974024

後続の私の書き込みの「トランジションサンプル」で、問題なく動作することが確認出来ましたのでご安心ください。

ライブラリがOKなのは、その問題がライブラリ内部に隠蔽され、十分スキルのあるライブラリメンテナが対処してくれるから。これは一般に、自前での再実装よりライブラリ利用の方が推奨される理由でもある。

何を選択するかは各々の自由です。ただ、パフォーマンス的に問題のある書き方だと認識すら出来ないのは問題です。認識できなければ選択することは出来ません。

従って一般の開発者に向けた記事で1.に反した実装を勧めるのは悪影響がある。

取捨選択は各々で行うべきことです。ライブラリ開発者以外はスキルが無いから悪影響というのは、あまりに他人を下に見過ぎではないでしょうか?プログラマの一般的な価値観やスキルは、我々が勝手に決めつけて良いものではありません。

ちなみに「Reactの基本思想」みたいなまた嫌われそうな言葉を使ってしまいましたが、Why did we build React?の内容をもとに私はそう理解しています。

時代が流れてReact18でSSR-Streamingとか、けっこう複雑な方へ流れていってます。ご興味があれば、是非こちらをお読みください。

https://zenn.dev/sora_kumo/articles/5ee8d38d6b2366

また、この書き方がOKなら、本文第1章タイトル「ContextAPI と useState は本来組み合わせてはいけない」はやはり間違い、 もしくは仮にそれ以外の意図があったとしてもミスリーディングではありますよね。

いえ、Contextでディスパッチャーを配っていることに問題は無いのかという話です。これに問題が無いなら、「無駄な再レンダリングを起こさないパターン」でも、ディスパッチャーを配ってコンポーネント外で値を入れてもらうという同じ動作を行っています。

fetchなどで外部データを受け取ったり、windowイベントを受け取ってステートをセットしたりと、Webアプリを作る上では多くのケースでコンポーネント外からイベントを受け取ってステートを書き換える構造になっています。ディスパッチャーの含まれた関数を渡して、イベント発生時にステートを書き換えてもらうわけです。結局やっていることは同じです。

図らずも前のコメントでリンクしたDon’t Overuse Refsはむしろこの論点に近いです。

それがこちらの文章に続くわけです。
Lifting State Up
お気づきいただけましたか?一番最後の「Lessons Learned」がわかりやすいと思います。つたない要約で申し訳ありませんが、「stateを中継していくことでコンポーネントの変化が可視化され、デバッグが容易になる」と書かれています。要約に問題があればツッコミを入れてください。

これに従うと、そもそものところでContextAPIの使用すら想定外の運用になってしまいます。当たり前と言えば当たり前です。これが書かれた頃はContextAPIが存在しなかったのですから。

ちなみに、メモ化が面倒という点はReact側も認識していて、自動でメモ化してくれるコンパイラ(React Forget)を実験中らしいです。

面白い技術ですね。逆に混沌としそうですが、そういうのをいじるのは大好きです。

Yuichiro TachibanaYuichiro Tachibana

ご返信ありがとうございます、なのですが各論はもういいんじゃないかと思いました。頂いた内容についての返信を下書きしたら、ほぼ今までのコメントの焼き直しになってしまいました。
永遠に話が噛み合わずにループするんだと思います。

この点で合意に至れないならこれ以上の深掘りは無理かなと思います。

全ての論点がここに行き着いた気がします。

再度書きますが、この議論は「根本的なところで合意できないということ」に合意して終着な気がしています。

お付き合いいただきありがとうございました。

空雲空雲

こちらこそ、ありがとうございました。
また何かあればよろしくお願いいたします。

空雲空雲

時間の出来たときに検証しますが、React18のトランジションに関しては
○「無駄な再レンダリングを起こさないパターン」のサンプル
× 「データを Context 側に持たせつつ、無駄な再レンダリングを防ぐパターン」のサンプル
で、コンポーネント内のuseStateを通していない後者の書き方が引っかかります。

こちらの記事の書き方は動くと思われますが、まだ未検証です
https://zenn.dev/sora_kumo/articles/52ff6d925468e8

空雲空雲

「無駄な再レンダリングを起こさないパターン」のサンプルに
こちらのトランジションのサンプルを拝借して正常動作するように合成してみました
https://qiita.com/uhyo/items/6a3b14950c1ef6974024
useRef内にディスパッチャーをため込んでデータを配ってますが、最終的にはコンポーネント内のstateを変更しているので問題なく動きます
codepenだと何故かreact@18設定で動かなかったので、codesandboxに入れています

https://codesandbox.io/s/toranzisiyonsanpuru-2cjtf?file=/src/pages/index.tsx

kazukinagatakazukinagata

@Yuichiro Tachibana さんがこんなに一生懸命に話しても平行線なので、もはや宗教なのかなと思いつつ、やはりアプリ開発の初心者がこの記事を見たら有害なんじゃないかなぁという感想だったのでコメントします。

そもそも不必要な(筆者の言葉を借りましたが、推測するにDOMの変更が必要ない再レンダーだと思っています)を過剰に忌み嫌っている気がありますが、それは親から子へ一方向に状態を伝え、状態が変わればコンポーネントツリーがリアクティブに変わる、というReactの設計思想そのものへの否定のように思えます。

強く言いたいのは再レンダーがたちまちパフォーマンスの低下を起こすわけではなく、本来の設計思想として許容されるべきものです。
中にはパフォーマンスが低下する場合もある、というのが正しく、もしパフォーマンスが低下した場合にも対処策としてmemoのようなAPIが用意されています。一方再レンダーの回避策としてのref、という話をreactアプリケーションという文脈で聞いたことがありません。


reduxの中でそのようなテクニックが使われているからアプリケーションでも使っていいでしょう、とはならないと思います。reduxは react と切り離された世界の状態管理ライブラリですので、その閉じた世界で、かつ、きちんとメインテナーがいる中で、reactとは違う状態管理を作るのは全くおかしなことではありません。そしてreduxとreactとの接続口としてrefなどが使われる、だけの話です。

一方、この記事の想定読者はライブラリ開発者ではなくreactアプリケーション開発者だと思われ、そういった開発者はreact流の状態管理(useStateやuseReducer)に乗っかるのが賢明だと思います。

親のstateの変更が子孫に伝わっていき、子孫の宣言的なviewが状態に応じて変わってくれる、親から子への状態の一方向性がReactの良さであって、提案されているトリッキーな書き方は、子が親の世話をしなければいけない、伝達が逆流するという意味でそれこそアンチパターンに見えます。自分がレビューするなら絶対にapproveしない実装でした。


親の再レンダーを回避しつつ子供のstateを更新する、一つのテクニックとしてこの方法を提案するなら何も言わなかったと思いますが、親がuseStateを使って子供を再レンダーすることがたちまちアンチパターンと言うなら、それは大きな誤解を初心者に与えて、奇妙なアプリケーション実装を世に生み出すことになるので、それは回避したいと思います。

fallfall

私は@Yuichiro Tachibanaさん側の意見を持っています。
最近Reactのドキュメントが一新されましたが、useContext経由で渡されたデータの更新には「state」を利用すると明示されています。
@kazukinagataさんのいう通り、1つの方法として紹介するのではなく、広く使われており、公式でも使用されている書き方をアンチパターンとするのは、誤解を生むタイトルだと感じます。
@Yuichiro Tachibanaの提示したユーザー認証においてContextの値を参照していない孫要素が再レンダリングにより発生する不都合 >>> 公式やReactの思想を無視したトリッキーな書き方だと思います。
reduxの真似事をしたいのであればreduxを使えば良いのです。

あのv0あのv0

再レンダリングが重くなるコンポーネントは特殊なケースのUIだけで、それらについてもuseMemoやreduxのuseSelector使えばいいと思います。
また、よほど重いコンポーネントでない限り再レンダリングすべきかどうかの比較関数よりも、再レンダリングしちゃった方が軽量です。