関数コンポーネントでは単純に関数が 2 回実行されます
しかし useEffect の関数の呼び出しは 1 回だけで副作用は 1 回しか起きないようになっていました
それが React 18 では 2 回呼び出されます
一度アンマウントしているのでちゃんとアンマウント時の処理を書いていれば問題にはならないはずです
const Component = () => {
useEffect(() => {
console.log("use effect")
return () => console.log("use effect return")
}, [])
return <div></div>
}
このコンポーネントを StrictMode で使用すると
use effect
use effect return
use effect
という風にログに出ます
個人的に困ってるのは 必要がない場合にはアンマウント時の処理を省略してることが多いからです
依存配列が空の場合はコンポーネントが生存中 最初の 1 度しか呼び出されないはずです
副作用で外部通信や window などのコンポーネント外にリスナをつけるのであればアンマウントの処理も行います
しかし 対象がコンポーネント内で管理する DOM の場合は アンマウントされれば要素は GC で消えるのでリスナの解除は不要です
なのでわざわざ書くことはほとんどなかったです
それが今回の変更で問題になります
例えばこのコード
const Component = () => {
const ref = useRef()
useEffect(() => {
const div = ref.current
div.addEventListener("click", () => {
console.log("clicked")
})
}, [])
return <div ref={ref}>click here</div>
}
div をクリックすると
clicked
clicked
と 2 回表示されます
一度アンマウントされたのなら対応する DOM も破棄して 再作成して欲しいものです
この変更は今後の新機能対応のためらしいです
その動作と StrictMode の一旦アンマウントする処理を無効化できればいいのですが 今のところ個別に無効化はできないみたいです
本来ちゃんと対応すべきなのかもしれませんが 簡単にアンマウント時の処理を書けないケースもあります
ライブラリだとそんな手段は用意されてなくて 要素消せばいいよみたいなもの普通にあったりしますしね
今はわかりませんが 以前は GoogleMaps がそんな感じだったと思います
手抜きの回避策としてはこういうのもありかもですね
const Component = () => {
const ref = useRef()
const initialized_ref = useRef(false)
useEffect(() => {
if (initialized_ref.current) return
initialized_ref.current = true
const div = ref.current
div.addEventListener("click", () => {
console.log("clicked")
})
}, [])
return <div ref={ref}>click here</div>
}
ただ StrictMode の動作としてならともかく 実際に発生する場合に div が新しくなる可能性が 0 なのかはわかりません
初期化時に要素に特定のプロパティを追加する系だとそれがあるかを見るなど 要素を見て初期化済みかの判断をするほうが良いかもしれません