背景
Hooks を使って管理していた key の値が無限に増幅され、無限に再レンダリングが発生するという事象に直面しました。
実装されていた処理を簡略化して記載すると下記のような形です。
const ParentPage: () => {
...
return (
<ChildComponent />
)
}
const ChildComponent = ({ input = [] }) => {
const [key, setKey] = React.useState(0)
const [something, setSomething] = useSomething(input)
const f = () => {
setSomething(...)
setKey(prev => prev + 1)
}
React.useEffect(() => {
if (...) {
f()
}
},[something]}
return <SomethingComponent key={key} />
}
※一部、処理を省略してしまっており、読みにくくて申し訳ありません。
やりたかったのは、何かの処理の時に SomethingComponent
を再描画させるために key の更新です。
ですが、このケースで無限ループが発生しました。
原因と解決策
原因
お気づきの方もいると思いますが、この記法だと eslint の exhaustive-deps
で警告が出ます。
ただ、今回のケースはこの deps で実現したいものだったのでそこは変えないようにします。
それを考慮した上で、今回の事象の原因は引数 input に与えたデフォルト引数に問題がありました。
input の値が string
や number
の場合は再レンダリングは発生しません。
ParentPage
で利用する際に <ChildComponent input={[]} />
のように prop として渡しても再レンダリングは発生しません。
ですが、デフォルト引数として []
や {}
を渡した場合、無限に再レンダリングします。
解決策
トップレベルに変数を用意して、その内容をデフォルト引数に渡します。
この方法なら、 ChildComponent
を別ファイルに切り出しても再レンダリングを防ぐことができます。
const defaultInput = []
const ChildComponent = ({ input = defaultInput }) => {
...
}
まとめと所感
まとめ
実際に利用していたケースでは処理がもう少し複雑だったので、原因究明に時間がかかりました。
デフォルト引数のインスタンスが再生成されるタイミングの理解不足でした。
- useState により状態が更新
- ChildComponent 関数が再度実行される
- useEffect の deps が状態の変化を判定
という流れになりますが、デフォルト引数でオブジェクトを指定している場合、 2. では毎回新たなインスタンスが生成されるため 3. で useEffect の処理が実行されてしまいます。
結果、再度 1. になりめでたく無限ループ…という流れでした。
所感
デフォルト引数は Airbnb のコーディングガイド の good ノウハウにも載っているので便利なテクニックです。
今回のケースのように React コンポーネントの props として利用する場合、かつ React 内部で Hooks と依存性を持たせる場合は無限ループにならない書き方ができているかを意識する重要性を感じました。
記載内容に不備・誤り、または指摘やアドバイスなどがありましたらコメントまでお願いします!