[React] React 18 から setInterval などの非同期処理で気をつけること

React 18 での変化点(の1つ)

コンポーネントがアンマウントされたあとに状態が更新されても、警告が出なくなりました。

でなくなった警告はこれです。

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

非同期処理の中断、終了処理が行われていないことに気づきにくくなりました

何が起きるか、どうするか

setInterval や setTimeout の処理がアンマウント後も残ることで、同じ処理が 2回、3回 と何回も呼ばれたり、処理が残り続けることでメモリリークしている状態になります。

Bad

ダメなパターンがこちらです。

例によって、 App.js を編集しています。

import { useEffect, useState } from 'react';
import './App.css';

function App() {

  const [date, setDate] = useState(new Date());

  useEffect(() => {
    setInterval(() => console.log('function called') || setDate(new Date()), 5000);
  }, [])

  return (
    <div className="App">
      <header className="App-header p-5">
        <h1>{date.toLocaleTimeString()}</h1>
      </header>
    </div>
  );
}

export default App;

確認でコンソールを見ると、2回 ずつ ログが出力されているのがわかります。

React StrictMode では、 App.js は マウント→アンマウント→マウント されますが、このとき setInterval が終了されずに残ったままになっています。

このサンプルでは 2回 ですが、ページ遷移などがあるような場合には、コンポーネントが再マウントされるたびに無限に呼ばれる回数が増えていきます 💦

good

良いパターンがこちら。

アンマウントされるときに、setInterval の処理を終了させる処理を書きます。

局所的なサンプル

return のところが大事。

  useEffect(() => {
    const timer = setInterval(() => console.log('function called') || setDate(new Date()), 5000);
    return () => clearInterval(timer);
  }, [])

ぜんたい

import { useEffect, useState } from 'react';
import './App.css';

function App() {

  const [date, setDate] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => console.log('function called') || setDate(new Date()), 5000);
    return () => clearInterval(timer);
  }, [])

  return (
    <div className="App">
      <header className="App-header p-5">
        <h1>{date.toLocaleTimeString()}</h1>
      </header>
    </div>
  );
}

export default App;

これで確認すると、ちゃんと 1回 ずつ呼ばれていることがわかります (/・ω・)/

はい、できました。