Functional ComponentでforceUpdateをエミュレートする
Functional ComponentでforceUpdateをエミュレートする
参考
- https://www.telerik.com/blogs/what-is-render-react-how-do-you-force-it?utm_source=reactdigest&utm_medium=email&utm_campaign=274
- https://reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate
- https://sung.codes/blog/2018/11/08/emulate-forceupdate-with-react-hooks/?preview_id=1931&preview_nonce=5e2a9a7f1b&preview=true&_thumbnail_id=1949#comment-4333911452
- https://www.reddit.com/r/reactjs/comments/6813z8/tell_me_any_real_use_case_where_forceupdate/dgwk210/?utm_source=reddit&utm_medium=web2x&context=3
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でも、useState
やuseReducer
を使えばエミュレートすることは可能。ただし非推奨
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> </> ); }