タイトルが長い!
3行
useCallback(()=> ,[])
とかuseEffect(()=>,[])
みたいにハンドラをメモる- ムダな再レンダリングが起こるのを防ぎたい
- イベントハンドラ内で外部変数(例えば
const [state, setState] = useState()
のstate
)を参照する - イベントが起こるたびにハンドラで参照しているstateは常に最新のものが来て欲しいのだが………
- こない
例
実例を書いた。
このデモは
- カウンターアプリ
- increment
- decrement
- トータルクリック回数もわかる
というもの。
『To use useCallback』と『To use useEventCallback』の2つがあって
- 『To use useCallback』
- 動きそうで動かない例
- 記事タイトルの現象が起こっている
- 『To use useEventCallback』
- 動くようにしたほうの例
- 記事タイトルの問題を力技で解決している
という内訳です。
とりあえずのところ『To use useCallback』のほうだけいまはみてほしい。
『To use useCallback』はタイトルの現象が起こって動かないのが正常
kwsk
export function ToUseUseCallback({ ...rest }) { const [count, setCount] = useState(0) const [clickCount, setClickCount] = useState(0) const onIncrement = useCallback(() => { setCount(count => count + 1) setClickCount(clickCount + 1) // ← ここ! }, []) const onDecrement = useCallback(() => { setCount(count => count - 1) setClickCount(clickCount + 1) // ← ここ! }, []) . . .
setClickCount(prevCount => prevCount + 1)
とも書けるけどそう書くと問題なく動いてしまう…なので例のためにわざとsetClickCount(clickCount + 1)
として関数の外側にあるstate値を参照している。
setClickCount(prevCount => prevCount + 1)
と書いた場合は実行時に常に最新の値を返してくれるから動く
clickCount
はメモ化された瞬間の値になるので、初期値が0だったらどれだけボタンを押しても永遠に0のままである。
なぜこんな挙動なのか
そりゃメモされたものを返しているからね…
仮に上記の例をごく当たり前に解決するには
export function ToUseUseCallback({ ...rest }) { const [count, setCount] = useState(0) const [clickCount, setClickCount] = useState(0) const onIncrement = useCallback(() => { setCount(count => count + 1) setClickCount(clickCount + 1) }, [clickCount]) // ← depsに指定する . . .
このようにdepsに依存している変数を書けば動く。
clickCount
に依存しているのだから第二引数の配列(deps)にclickCount
を指定してやれば当然clickCount
が変わったら関数は新しくメモされるので期待した現在のclickCount
値になる。
だがしかし、こんなカウンター程度ならまだいいものの、もっと頻繁に値が変わったりするものに依存している場合、もはやメモ化している意味がなくなってしまう…… 😇
例えばuseCallback
ではなくuseEffect
を用いてなにかしらsubscribeするときに、こういう風にタイトルの問題を回避するために外部変数を第二引数(deps)に指定すると、depsが変わるたびにsubscribeが発生してしまう……。
Issue & FAQ
言及はすでにいろいろある。
いろいろと議論されている
https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
公式のFAQに書かれている事柄でもある
それで我々はどうすればいいのか
FAQにかかれているようにuseEventCallback
というカスタムフックを作って、若干のハック技とともに回避できるよというはなし。
ただこれはエスケープハッチとして言及されているだけで推奨はされていない。
In either case, we don’t recommend this pattern and only show it here for completeness. Instead, it is preferable to avoid passing callbacks deep down.
と書かれていて、できるだけuseReducer
を用いてdispatchして処理を外に出すほうが良いらしい。
上記で貼ったIssueでDan氏が言っているけれどreducerはrenderフェーズで実行されることが確約されているのでということらしい。
これは並行モード(concurrent mode)というのが次期Reactで実装される予定に絡むはなしだそうで、useEventCallback
カスタムフックでハックするのはイケてないとのこと。
とはいえuseReducer使うほどでもないときもありそうなので
勝手を知ったうえでuseEventCallback
を使うのは自己責任という免罪符付きでアリな場面もありそう(実際に使いたい場面があった)。
ただ上記の公式FAQのuseEVentCallback
のコードexampleはuseCallbackの代替なのに
コールバックに引数を渡せないコードなので困ってしまった。
引数を渡せるuseEventCallback
を作った
引数渡せなくて困ったので渡せるタイプのヤツをTSで自分で書いた。
index.tsがソレ
npmにあげてない(エスケープハッチだから)ので、もし利用したい方は自己責任でコピペしてくださいという気持ちです。
ちなみに若干話がそれるけれどuseEventCallbackWithUseEffect
というものも一応exportしています。
import {useEventCallback, useEventCallbackWithUseEffect} from './your-directories/useEventCallback'
- useEventCallback
- 内部では
useLayoutEffect
を使用
- 内部では
- useEventCallbackWithUseEffect
- 内部では
useEffect
を使用
- 内部では
という違い。
React公式FAQに書いてあるコード例ではuseLayoutEffect
を使っていて、これはrenderフェーズを確約させるためのような気がしている。
ただuseLayoutEffect
はブロッキングが発生するので、どうなんだろうと思ってuseEffect
を使うuseEventCallbackWithUseEffect
も一応用意した感じ。
自分が実際に使いたかった場面はちょっと特殊で、renderフェーズ待たなくてもよさそうでuseEffect
でも問題なさそうだったからこっちも用意したという背景がある。
この
useLayoutEffect
使うかuseEffect
使うかあたりの話、ハッキリと腹落ちしてないので知見あるかたは教えて欲しいです
話は戻って
そういうわけで、経緯がややこしく長くなりましたが再度例をみてみましょう。
『To use useEventCallback』のほうのカウンターアプリは"動く例"でした。
なるべく簡素なexampleにしたのでこの例では引数を渡していませんが…。
トータルクリック回数がちゃんとカウントされて動いています。エスケープハッチでも問題なさそうであればuseEventCallback
を使えば動いてよかったですね。
こんだけ長く書いてアレですが
useEventCallback
ハックは公式に非推奨と書かれていることなのであまり積極的に使うべきではない…
そもそも例えばハンドラ内でprev値を元に新しくstateを更新したいとかなら
const App = ()=> { const [count, setCount] = useState(0) const onClick = useCallback(()=> { setCount(prev => prev + 1) }, [] ) . . . }
という風にstateのアップデーターを関数式にすればいいので…。
とはいえstateの更新だけではなく外部変数を元になんかやるみたいなときには使いたくなる場面があるかもしれないので書いた。
ご利用は計画的にということで。