React Hooks中的useEffect()如何仅用于componentWillUnmount时的清理工作?

198

让我简单解释一下这段代码的结果,以便更容易地说明我的问题。

const ForExample = () => {
    const [name, setName] = useState('');
    const [username, setUsername] = useState('');

    useEffect(() => {
        console.log('effect');
        console.log({
            name,
            username
        });

        return () => {
            console.log('cleaned up');
            console.log({
                name,
                username
            });
        };
    }, [username]);

    const handleName = e => {
        const { value } = e.target;

        setName(value);
    };

    const handleUsername = e => {
        const { value } = e.target;

        setUsername(value);
    };

    return (
        <div>
            <div>
                <input value={name} onChange={handleName} />
                <input value={username} onChange={handleUsername} />
            </div>
            <div>
                <div>
                    <span>{name}</span>
                </div>
                <div>
                    <span>{username}</span>
                </div>
            </div>
        </div>
    );
};

ForExample组件挂载时,将记录“effect”。这与componentDidMount()相关。

每当我更改名称输入时,都会记录“effect”和“cleaned up”。反之亦然,在我将[username]添加到useEffect()的第二个参数后,更改用户名输入时不会记录任何消息。这与componentDidUpdate()相关。

最后,当ForExample组件卸载时,将记录“cleaned up”。这与componentWillUnmount()相关。

我们都知道这一点。

总之,“cleaned up”在重新呈现组件(包括卸载)时被调用。

如果我想使此组件仅在卸载时记录“cleaned up”,只需将useEffect()的第二个参数更改为[]

但是如果我将[username]更改为[],则ForExample组件不再实现名称输入的componentDidUpdate()

我的目标是使该组件支持名称输入的componentDidUpdate()以及componentWillUnmount()(仅在组件被卸载时记录“cleaned up”)。


8
你可以有两个不同的效果。一个是给出一个包含username作为第二个参数的数组,另一个是给出一个空数组作为第二个参数。 - Tholle
1
是的,这是一种解决方法。 - Tholle
1
@Tholle 我以为最后的useEffect()方法会覆盖它。我会尝试的。谢谢。 - koo
@Tholle 它有效。再次感谢。顺便问一下,有没有更漂亮的实现方式?感觉我们写了两次同名方法。 - koo
2
太好了!不用谢。这取决于清理应该做什么。两个独立的效果并不是一个坏的解决方案。 - Tholle
显示剩余4条评论
12个回答

265

你可以使用多个 useEffect()。

例如,如果我的变量是data1,我可以在组件中使用所有这些:

useEffect( () => console.log("mount"), [] );
useEffect( () => console.log("data1 update"), [ data1 ] );
useEffect( () => console.log("any update") );
useEffect( () => () => console.log("data1 update or unmount"), [ data1 ] );
useEffect( () => () => console.log("unmount"), [] );

13
第一个useEffect和最后一个useEffect的区别是什么?第一个useEffect会在willmountdidmount时被调用,而最后一个useEffect返回一个回调函数,并且使用一个空数组。为什么?您能详细说明每个useEffect的用例以及何时以及如何使用吗? - Siluveru Kiran Kumar
8
回调函数的返回值是在销毁(卸载事件)时被调用的。这就是为什么最后一个示例是HOC,立即返回函数的原因。第二个参数是React用来查找更改以重新运行此钩子的位置。当它是一个空数组时,它会运行一次。 - Georgy
4
谢谢@Georgy,我明白了,最后一个useEffect返回的是回调函数,但看得不够清楚。 - Siluveru Kiran Kumar
4
如果你使用useEffect钩子,并且它返回一个函数,那么返回函数之前的代码会被视为componentDidMount,而返回函数中的代码会在componentWillUnmount时调用。如果我理解得正确的话,这有点令人困惑。代码示例如下: useEffect(() => { // code to run on mount return () => { // code to run on unmount } })是这样吗? - Maiya
3
我建议阅读https://overreacted.io/a-complete-guide-to-useeffect/思考钩子在组件生命周期中的作用并不是一件很愉快的事情。 - moto
显示剩余3条评论

157

由于清理不依赖于username,您可以将清理放在一个单独的useEffect中,并将空数组作为第二个参数传递。

示例

const { useState, useEffect } = React;

const ForExample = () => {
  const [name, setName] = useState("");
  const [username, setUsername] = useState("");

  useEffect(
    () => {
      console.log("effect");
    },
    [username]
  );

  useEffect(() => {
    return () => {
      console.log("cleaned up");
    };
  }, []);

  const handleName = e => {
    const { value } = e.target;

    setName(value);
  };

  const handleUsername = e => {
    const { value } = e.target;

    setUsername(value);
  };

  return (
    <div>
      <div>
        <input value={name} onChange={handleName} />
        <input value={username} onChange={handleUsername} />
      </div>
      <div>
        <div>
          <span>{name}</span>
        </div>
        <div>
          <span>{username}</span>
        </div>
      </div>
    </div>
  );
};

function App() {
  const [shouldRender, setShouldRender] = useState(true);

  useEffect(() => {
    setTimeout(() => {
      setShouldRender(false);
    }, 5000);
  }, []);

  return shouldRender ? <ForExample /> : null;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>


1
很好的干净的例子。不过我在想,我能否在导航改变时以某种方式触发useEffect,还是必须将其移动到组件树中?因为当我只粘贴您“清理后”的useEffect时,我没有看到这个触发器。 - Fabian Bosler

34
为了补充接受的答案,我曾经遇到过类似的问题,并使用下面这个虚构的例子采用了类似的方法解决了它。在这种情况下,我需要在componentWillUnmount上记录一些参数,并且如原始问题所描述的,我不想每次参数更改时都进行记录。
const componentWillUnmount = useRef(false)

// This is componentWillUnmount
useEffect(() => {
    return () => {
        componentWillUnmount.current = true
    }
}, [])

useEffect(() => {
    return () => {
        // This line only evaluates to true after the componentWillUnmount happens 
        if (componentWillUnmount.current) {
            console.log(params)
        }
    }

}, [params]) // This dependency guarantees that when the componentWillUnmount fires it will log the latest params

4
useEffect的顺序很重要,如果有人想知道的话。 - sirclesam

13
你可以简单地写成:
  useEffect(() => {
  return () => {};
  }, []);

1
这是正确的答案。请参见此链接 - Francisco Jesus
是的,这就是正确答案。 - BennKingy

4
使用自定义JS事件,即使有依赖项,也可以模拟卸载componentWillUnmount。
问题:
    useEffect(() => {
    //Dependent Code
    return () => {
        // Desired to perform action on unmount only 'componentWillUnmount' 
        // But it does not
        if(somethingChanged){
            // Perform an Action only if something changed
        }
    }
},[somethingChanged]);

解决方案:

// Rewrite this code  to arrange emulate this behaviour

// Decoupling using events
useEffect( () => {
    return () => {
        // Executed only when component unmounts,
        let e = new Event("componentUnmount");
        document.dispatchEvent(e);
    }
}, []);

useEffect( () => {
    function doOnUnmount(){
        if(somethingChanged){
            // Perform an Action only if something changed
        }
    }

    document.addEventListener("componentUnmount",doOnUnmount);
    return () => {
        // This is done whenever value of somethingChanged changes
        document.removeEventListener("componentUnmount",doOnUnmount);
    }

}, [somethingChanged])

注意事项:必须按顺序使用useEffects,没有依赖关系的useEffect必须先编写,以避免在其被移除后仍调用该事件。


1
这个答案最重要的部分是警告您必须使用useEffect钩子。 - brycejl

3
function LegoComponent() {

  const [lego, setLegos] = React.useState([])

  React.useEffect(() => {
    let isSubscribed = true
    fetchLegos().then( legos=> {
      if (isSubscribed) {
        setLegos(legos)
      }
    })
    return () => isSubscribed = false
  }, []);

  return (
    <ul>
    {legos.map(lego=> <li>{lego}</li>)}
    </ul>
  )
}

在上面的代码中,fetchLegos函数返回一个Promise。我们可以在useEffect的作用域中加入条件语句,来“取消”该Promise,防止组件卸载后应用程序进行状态更新。
警告: 无法在已卸载的组件上执行React状态更新。这是一个no-op,但它表示你的应用程序存在内存泄漏。为了解决这个问题,在useEffect清理函数中取消所有订阅和异步任务。

2

这是我的解决方案,通用化为一个自定义钩子:

import React, { useEffect, useRef } from 'react';

const useUnmountEffect = (effect, dependencies) => {
  if (typeof effect !== 'function') {
    console.error('Effect must be a function');
  }

  const componentWillUnmount = useRef(false)

  useEffect(() => () => {
    componentWillUnmount.current = true
  }, []);

  useEffect(() => () => {
    if (componentWillUnmount.current) {
      effect?.();
    }
  }, dependencies);
}

export default useUnmountEffect;

2

我不是创建太多复杂的函数和方法,而是创建一个事件监听器,并自动为我完成挂载和卸载,无需手动担心。以下是一个例子。

//componentDidMount
useEffect( () => {

    window.addEventListener("load",  pageLoad);

    //component will unmount
    return () => {
       
        window.removeEventListener("load", pageLoad);
    }

 });

现在这部分已经完成,我只需要像这样从pageLoad函数中运行任何我想要的内容。
const pageLoad = () =>{
console.log(I was mounted and unmounted automatically :D)}

1

这个怎么样:

function useOnUnmount(callback: () => void) {
    const onUnmount = useRef<(() => void) | null>(null);
    onUnmount.current = callback;

    useEffect(() => {
        return () => onUnmount.current?.();
    }, []);
}

useOnUnmount(() => {
    console.log("unmount", props);
});

0

欢迎提供解决方案的链接,但请确保您的答案即使没有链接也是有用的:在链接周围添加上下文,以便其他用户知道它是什么以及为什么存在,然后引用您链接的页面中最相关的部分,以防目标页面不可用。仅仅是一个链接的答案可能会被删除 - rizerphe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接