メモリリーク対応の共通化

公開日: @fukusan0901
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 the componentWillUnmount method.

React 開発ではよくこんな感じのメモリリークしてますよ系のエラーが出ると思います。メモリリークとはIT 用語辞典によると、「コンピュータで実行中のプログラムが確保したメモリ領域の解放を忘れたまま放置してしまうこと。動作の不具合を招くバグ(欠陥)の一種。」と記載されています。つまり、開発中のアプリケーションにて何らかの処理がメモリ領域の解放を忘れたまま実行されていますよというエラーなわけです。

どういうときにメモリリークするのか

いくつかのパターンが考えられますが、useEffect 内で useState を更新しようとする処理を入れているがコンポーネントのアンマウント時の処理を入れていない場合とかで起こりますよね。(useEffect の中で state を更新する処理を入れるような実装は育ちが悪いと思われかねないのでやめましょう)また、あるコンポーネントに非同期処理を入れていて、裏で非同期処理が走っているのにコンポーネントがアンマウントされてしまったなどの場合などもあると思います。せっかくなので後者のパターンを想定してみましょう。

実装

今回、モーダル内に非同期処理を入れていて、ユーザーがモーダルが閉じるボタンを押したことによりモーダルのコンポーネントがアンマウントされたという状況を想定します。想定するのはこの状況ですが、メモリリークは結構起こり得るので処理を抽象化するためにカスタムフック化したいと思います。

const handleOnClick = useCallback(async () => {
  const body = {
    method: 'POST',
    body: JSON.stringify({ name: 'ふくさん', age: '28' }),
  }
  const res = await fetch('/user', body)
  if (res.ok) {
    // 何らかの処理
  } else {
    // 何らかの処理
  }
}, [])

上記のような処理がモーダル内で行われており、fetch 関数を実行しているときにモーダルが閉じられたと仮定します。このままだと、res.ok以下の処理を行いたいのにコンポーネントがアンマウントされておりこちらの関数がメモリリークしちゃいますね。そこで、fetch 処理以下にこのコンポーネントがアンマウントされたかどうかの判定を入れて、アンマウントされていれば処理を中断するような実装に変更したいと思います。

判定するカスタムフックは以下のような感じです。マウント中か、アンマウントされたかを bool 値で返します。

import { useCallback, useEffect, useRef } from 'react'

export const useMounted = () => {
  const mountedRef = useRef(false)

  useEffect(() => {
    mountedRef.current = true
    return () => {
      mountedRef.current = false
    }
  }, [])

  const isMounted = useCallback(() => mountedRef.current, [])

  return isMounted
}

上のカスタムフックを使って先程の処理を書き直すと以下のような感じになります。

const isMounted = useMounted()

const handleOnClick = useCallback(async () => {
  const body = {
    method: 'POST',
    body: JSON.stringify({ name: 'まつおふくたろう', age: '28' }),
  }
  const res = await fetch('/api/user', body)

  if (!isMounted()) {
    alert('モーダル閉じないで...')
  }

  if (res.ok) {
    // 何らかの処理
  } else {
    // 何らかの処理
  }
}, [])

まず、該当のコンポーネントでカスタムフックを呼び出します。そしてその関数を実行する必要があるのですが、その関数自身は現在のコンポーネントがマウントされているか、アンマウントされたかの判定を bool 値を返すので、実行と同時にアンマウントされた場合の条件分岐でそのまま使用できます。いい感じですね。

で、このカスタムフックがやっていることはシンプルでこのコンポーネントそのものを useRef で取得します。現在の状況をref.currentで確認して、マウントされているならtrueを、アンマウントされているならfalseを返します。useEffect 内での return 処理内はコンポーネントがアンマウントされたら実行されます。つまり、return 処理内に false を入れる処理を書いてあげれば汎用的になるというわけです。いい感じですね。