Functional ComponentでforceUpdateをエミュレートする

Functional ComponentでforceUpdateをエミュレートする

参考

Class ComponentのforceUpdate

実際にこのAPIを使うケースはほとんどないと思う。setStateを使ってもうまくコンポーネントが更新されないのでこのAPIを使いたい、といった場合はオブジェクトの同じ参照を渡していないかまずはチェックしてほしい。

Reactコアメンバーによれば、レガシーなコードとReactとの統合で使用するらしいが、幸運にもそのようなケースに出くわしたことはない。

export default class App extends React.Component {
  onClickHandler = () => {
    this.forceUpdate();
  };

  render() {
    return (
        <button onClick={this.onClickHandler}>Click me</button>
    );
  }
}

Functional ComponentのforceUpdate

Functional Componentでも、useStateuseReducerを使えばエミュレートすることは可能。ただし非推奨

const useForceUpdate = (): [() => void, number] => {
  const [count, setCount] = React.useState(0);

  const increment = () => setCount((prev) => prev + 1);
  return [increment, count];
};

export default function App() {
  const [forceUpdate] = useForceUpdate();
  const onClickHandler = () => {
    forceUpdate();
  };
  return (
      <button onClick={onClickHandler}>Click me</button>
  );
}

Reactの描画プロセス

ではなぜ非推奨のAPIを紹介したのかというと、このforceUpdateがReactの描画プロセスを理解するのに役立つと思ったから。

Reactの描画プロセスは次の流れに沿って行われる:

  • Render: renderメソッドを実行した結果を集め、オブジェクトツリーを形成する
  • Reconciliation: 新しいオブジェクトツリーと前回との差分を検出する
  • Commit: 差分を実際のDOMに反映する

ここで重要なのは仮想DOMの差分だけが実際にDOMに反映されるということ。上記で紹介したサンプルコードでいくらforceUpdateを実行しても、<button>が書き換えられることはない。DOMインスペクタで検証してみてもDOMの更新を意味するflashが表示されていないことが分かる。

ただし、次のように関数の実行によって参照が変わるコンポーネントを使用すると、変更がなさそうに見えても差分は一旦破棄(アンマウント)され実際のDOMは更新される。

import * as React from "react";
import "./styles.css";

const useForceUpdate = (): [() => void, number] => {
  const [count, setCount] = React.useState(0);

  const increment = () => setCount((prev) => prev + 1);
  return [increment, count];
};

export default function App() {
  const [forceUpdate] = useForceUpdate();
  const onClickHandler = () => {
    forceUpdate();
  };
  const Child = () => <div>child</div>;
  return (
    <>
      <Child />
      <button onClick={onClickHandler}>Click me</button>
    </>
  );
}