React: Concept: Hook: Avoid Updating State after Unmount

28th February 2021 at 10:23am
React: Concept: Hook

避免在 unmounted 组件上更新 state,因为这是无意义的:

export default Component = () => {
    const [ data, setData ] = useState(null);
    useEffect(() => {
        (async () {
            setData(await someAsyncApiCall());
        })();
    }, []);
    return (<span>{ data }</span>);
}

如果 someAsyncApiCall() 返回数据时,组件已经被 unmount 的话,React 会在控制台输出警告:

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.

这篇 帖子 给了一个解决方法,使用一个 mount 变量来跟踪组件的生命周期:

export default Component = () => {
    const [ data, setData ] = useState(null);
    useEffect(() => {
        let mounted = true; // Indicate the mount state
        (async () {
            const data = await someAsyncApiCall();
            if (!mounted) return; // Exits if comp. is not mounted
            setData(data);
        })();
        return () => { // Runs when component will unmount
            mounted = false;
        };
    }, []);
    return (<span>{ data }</span>);
}

mounted 变量不能是定义在 useEffect() 外。不然会报这样的错误:

Assignments to the 'mounted' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect.

但如果你有在 useEffect 外使用 mounted 变量的需求,你可以用 useRef 来传递一个 mutable value。比如下面的组件:

  1. 在组件 mount 时发起 API 请求,加载笔记列表
  2. 另外一个函数发起 API 请求,新增一条笔记

它们都需要考虑组件是否 mount:

import React, { useEffect, useRef, useState } from "react"

import api from "utils/api"

export default function Home () {
  const [notes, setNotes] = useState([])

  let mountedRef = useRef(true)
  useEffect(() => {
      api.get("/notes")
        .then(data => {
          if (!mountedRef.current) {
            return
          }
          setNotes(data)
        })
      return () => {
        mountedRef.current = false
      }
    },
    []
  )

  const handleCreatingNewNote = (e) => {
    api.post("/notes", { content: content })
      .then(data => {
        if (!mountedRef.current) {
          return
        }

        setNotes(prevNotes => {
          return [data, ...prevNotes]
        })
      })
  }

  return (
    // ...
  )
}