Developers using React must be aware of Warning: Can’t call setState (or forceUpdate) 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. This console warning is familiar, and it usually occurs in asynchronous scenarios. Specifically, we run the risk of seeing this warning when we try to setState the current component state in the callback of a timer or Ajax request. Because by the time setState is actually called back, our component may have been uninstalled. So how do we deal with this problem?

In general, the occasional Warning does not cause a serious performance problem, but if setInterval handles are not cleaned properly during the uninstall cycle, it will continue to leak even after your component is destroyed. It also slows down the response time of your project. Therefore, as serious developers, we need to consider these issues first when implementing logic. For those of you with OCD, you don’t want to see a red screen every time you open the console, not to mention another common Warning scenario where Element is missing a key in the rendering of array structures.

After a wave of science surfing the Internet, I have roughly two approaches, “treating the symptoms” and “treating the root cause.”

Treats the

This essentially means declaring a sentinel variable in your class component or in the hooks function component. It doesn’t matter if you declare it on instance attributes, useRef, useEffect local variables, they all achieve the same effect.

Let’s take a background log retrieval scenario as an example.

The class components:

export default class LogList extends PureComponent {
    _isMounted = false
    componentDidMount() {
        this._isMounted = true
    }
    componentWillUnmount() {
        this._isMounted = false
    }
    fetchLogList = id= > {
        return axios.get(`/fetchList/${id}`).then(res= > {
            if (this._isMounted) {
                // setState action...}})}render() {
        / / rendering}}Copy the code

Hooks components:

// useRef saves the sentry variable
export default function LogList() {
    const _isMounted = useRef(false)
    const [logList, setLogList] = useState([])
    fetchLogList = id= > {
        return axios.get(`/fetchList/${id}`).then(res= > {
            if (_isMounted.current) {
                // setLogList...
            }
        })
    }
    useEffect(() = > {
        _isMounted.current = true
        return () = > {
            _isMounted.current = false}}, [])return (
        / / rendering)}Copy the code
// useEffect internally declares the sentry variable
export default function LogList() {
    const [logList, setLogList] = useState([])
    fetchLogList = id= > {
        return axios.get(`/fetchList/${id}`)
    }
    useEffect(() = > {
        let _isMounted = true
        fetchLogList(1).then(res= > {
            if (_isMounted) {
                // setLogList...}})return () = > {
            _isMounted = false}}, [])return (
        / / rendering)}Copy the code

cure

How to cure the root cause? In fact, after looking closely at the operation in the palliative, we found that we were both mounting a “dirt” on the current component. As a localization of the component itself, it is no longer pure, and it is not appropriate for us to add things to the component itself in order to handle such asynchronously rendered warnings. The core idea of adjustment is to “decouple” **.

By referring to the timer itself, we can see that they all return a handler handle for later cancellation tasks. The same goes for promise returns such as Ajax requests, so the question becomes: How do we provide a way to cancel promises? In terms of design itself, such asynchronous waiting tasks should have a cancellation mechanism. If I wait too long, should I directly cancel the task and then initiate it? In addition, the task waiting processing logic itself should not be placed on the component properties to do, which will make a component design function is not centralized, it looks uncomfortable.

The general approach is to implement a higher-order function that returns the wrapped new Promise instance and the cancel method that supports cancelling the Promise:

const makeCancelable = (promise) = > {
    let hasCanceled_ = false;

    const wrappedPromise = new Promise((resolve, reject) = > {
        promise.then(
            val= > hasCanceled_ ? reject({ isCanceled: true }) : resolve(val),
            error= > hasCanceled_ ? reject({ isCanceled: true }) : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel() {
            hasCanceled_ = true; }}; };Copy the code

The previous problem can be modified as follows:

import { makeCancelable } from '@/utils'

export default function LogList() {
    const [logList, setLogList] = useState([])
    fetchLogList = id= > {
        return axios.get(`/fetchList/${id}`)
    }
    useEffect(() = > {
        const { promise, cancel } = makeCancelable(fetchLogList(1))
        promise.then(res= > {
            // setLogList...
        })
        return () = > {
            cancel()
        }
    }, [])
    return (
        / / rendering)}Copy the code

P.S. In fact, we can also change our thinking. By handing over the state to the Store of React-Redux, components can be divided into stateless components for display and rendering, outer business components can distribute actions through Dispatch, and middleware layer can carry out asynchronous actions, which can also deal with this problem.