🚨

React18では setState 時のメモリリーク対策は必要ない

3 min read

この記事は React Advent Calendar 2021 12日目の記事です。

非同期処理の結果などによって unmount されたコンポーネントで setState を実行するとお馴染みの警告が表示されますが、React18からは表示されなくなります。

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.

結論

React17以前でもこの警告文に対する対策は基本的に不要なので無視して良い。
React18にバージョンアップすると表示されなくなる。

https://github.com/reactwg/react-18/discussions/82

試してみる

Next.jsでサクッとReact18のbetaが使えるので試してみます。

https://nextjs.org/docs/advanced-features/react-18#react-18-usage-in-nextjs

まずは react v17.0.2 で警告文が表示されるコードを書きます。
画面遷移で以下のフローを実行するコードを用意します。

  1. 2秒後に setState を実行するボタンを用意
  2. 2秒経つ前に画面遷移でコンポーネントを unmount する
Home.tsx
const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <h1>Home</h1>
      <Link href="/test">Test</Link>
    </div>
  )
}
Test.tsx
const sleep = async (ms: number) => {
  return new Promise((resolve) => setTimeout(() => resolve(null), ms));
};

const Test: NextPage = () => {
  const [loading, setLoading] = useState(false);
  const handleClick = useCallback(async () => {
    setLoading(true);
    await sleep(2000);
    setLoading(false);
  }, []);

  return (
    <div className={styles.container}>
      <h1>Test</h1>
      <Link href="/">Home</Link>
      <div>
        <button onClick={handleClick}>Post</button>
      </div>
    </div>
  );
};

react17.0.2

想定通り警告文が表示されます。
では次にReact18で試してみます。
私が検証した環境は React v18.0.0-beta-24dd07bd2-20211208 です。

npm install next@latest react@beta react-dom@beta

React18-beta

警告文が表示されないことを確認できました。

解説

先ほどのissueによるとこの警告文は元々EventListenerなどのsubscribeに対してクリーンアップ処理を忘れていることに対する警告だったようです。

useEffect(() => {
  function handleChange() {
     setState(store.getState())
  }
  store.subscribe(handleChange)
  return () => store.unsubscribe(handleChange)
}, [])

クリーンアップを忘れているとメモリリークに繋がり得るのでこの警告文自体は正しかったのですが、大抵の場合はクリーンアップを忘れるよりも unmount 後に非同期処理が解決されることによって警告が表示されるパターンが頻発しました。

そうして本来の意図が誤解されたまま mount 状態でなければ setState を実行しないという警告を抑制するためだけの解決策が生まれました。

const isMountedRef = useRef(false)
useEffect(() => {
  isMountedRef.current = true
  return () => {
    isMountedRef.current = false
  }
}, [])

const handleClick = useCallback(async () => {
  setLoading(true);
  await someApi();
  if (isMountedRef.current) {
    setLoading(false);
  }
}, []);

このようなパターンではそもそもメモリリークに繋がらないため mount 状態を管理するのは不要であり、Reactのバージョンアップによって警告文も表示されなくなります。
そのため、可読性を下げて将来的にバグに繋がる可能性があるコードは書かずに静かにReact18を待ちましょう。

Discussion

ログインするとコメントできます