问题

我正在React中编写一个应用程序,无法避免一个超级常见的陷阱,即在componentWillUnmount(…)之后调用setState(…)。

我非常仔细地查看了我的代码,并试图在适当的位置放置一些保护子句,但问题仍然存在,我仍然观察到警告。

因此,我有两个问题:

我如何从堆栈跟踪中找出哪个特定的组件和事件处理程序或生命周期钩子对违反规则负责? 好吧,如何修复问题本身,因为我的代码在编写时就考虑到了这个陷阱,并且已经试图防止它,但一些底层组件仍然生成警告。

浏览器控制台

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.
    in TextLayerInternal (created by Context.Consumer)
    in TextLayer (created by PageInternal) index.js:1446
d/console[e]
index.js:1446
warningWithoutStack
react-dom.development.js:520
warnAboutUpdateOnUnmounted
react-dom.development.js:18238
scheduleWork
react-dom.development.js:19684
enqueueSetState
react-dom.development.js:12936
./node_modules/react/cjs/react.development.js/Component.prototype.setState
react.development.js:356
_callee$
TextLayer.js:97
tryCatch
runtime.js:63
invoke
runtime.js:282
defineIteratorMethods/</prototype[method]
runtime.js:116
asyncGeneratorStep
asyncToGenerator.js:3
_throw
asyncToGenerator.js:29

Code

Book.tsx

import { throttle } from 'lodash';
import * as React from 'react';
import { AutoWidthPdf } from '../shared/AutoWidthPdf';
import BookCommandPanel from '../shared/BookCommandPanel';
import BookTextPath from '../static/pdf/sde.pdf';
import './Book.css';

const DEFAULT_WIDTH = 140;

class Book extends React.Component {
  setDivSizeThrottleable: () => void;
  pdfWrapper: HTMLDivElement | null = null;
  isComponentMounted: boolean = false;
  state = {
    hidden: true,
    pdfWidth: DEFAULT_WIDTH,
  };

  constructor(props: any) {
    super(props);
    this.setDivSizeThrottleable = throttle(
      () => {
        if (this.isComponentMounted) {
          this.setState({
            pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
          });
        }
      },
      500,
    );
  }

  componentDidMount = () => {
    this.isComponentMounted = true;
    this.setDivSizeThrottleable();
    window.addEventListener("resize", this.setDivSizeThrottleable);
  };

  componentWillUnmount = () => {
    this.isComponentMounted = false;
    window.removeEventListener("resize", this.setDivSizeThrottleable);
  };

  render = () => (
    <div className="Book">
      { this.state.hidden && <div className="Book__LoadNotification centered">Book is being loaded...</div> }

      <div className={this.getPdfContentContainerClassName()}>
        <BookCommandPanel
          bookTextPath={BookTextPath}
          />

        <div className="Book__PdfContent" ref={ref => this.pdfWrapper = ref}>
          <AutoWidthPdf
            file={BookTextPath}
            width={this.state.pdfWidth}
            onLoadSuccess={(_: any) => this.onDocumentComplete()}
            />
        </div>

        <BookCommandPanel
          bookTextPath={BookTextPath}
          />
      </div>
    </div>
  );

  getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';

  onDocumentComplete = () => {
    try {
      this.setState({ hidden: false });
      this.setDivSizeThrottleable();
    } catch (caughtError) {
      console.warn({ caughtError });
    }
  };
}

export default Book;

AutoWidthPdf.tsx

import * as React from 'react';
import { Document, Page, pdfjs } from 'react-pdf';

pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;

interface IProps {
  file: string;
  width: number;
  onLoadSuccess: (pdf: any) => void;
}
export class AutoWidthPdf extends React.Component<IProps> {
  render = () => (
    <Document
      file={this.props.file}
      onLoadSuccess={(_: any) => this.props.onLoadSuccess(_)}
      >
      <Page
        pageNumber={1}
        width={this.props.width}
        />
    </Document>
  );
}

更新1:取消可节流功能(仍然没有运气)

const DEFAULT_WIDTH = 140;

class Book extends React.Component {
  setDivSizeThrottleable: ((() => void) & Cancelable) | undefined;
  pdfWrapper: HTMLDivElement | null = null;
  state = {
    hidden: true,
    pdfWidth: DEFAULT_WIDTH,
  };

  componentDidMount = () => {
    this.setDivSizeThrottleable = throttle(
      () => {
        this.setState({
          pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
        });
      },
      500,
    );

    this.setDivSizeThrottleable();
    window.addEventListener("resize", this.setDivSizeThrottleable);
  };

  componentWillUnmount = () => {
    window.removeEventListener("resize", this.setDivSizeThrottleable!);
    this.setDivSizeThrottleable!.cancel();
    this.setDivSizeThrottleable = undefined;
  };

  render = () => (
    <div className="Book">
      { this.state.hidden && <div className="Book__LoadNotification centered">Book is being loaded...</div> }

      <div className={this.getPdfContentContainerClassName()}>
        <BookCommandPanel
          BookTextPath={BookTextPath}
          />

        <div className="Book__PdfContent" ref={ref => this.pdfWrapper = ref}>
          <AutoWidthPdf
            file={BookTextPath}
            width={this.state.pdfWidth}
            onLoadSuccess={(_: any) => this.onDocumentComplete()}
            />
        </div>

        <BookCommandPanel
          BookTextPath={BookTextPath}
          />
      </div>
    </div>
  );

  getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';

  onDocumentComplete = () => {
    try {
      this.setState({ hidden: false });
      this.setDivSizeThrottleable!();
    } catch (caughtError) {
      console.warn({ caughtError });
    }
  };
}

export default Book;

当前回答

根据React文档,检查组件是否被挂载实际上是一个反模式。setState警告的解决方案是利用AbortController的使用:

useEffect(() => {
  const abortController = new AbortController()   // creating an AbortController
  fetch(url, { signal: abortController.signal })  // passing the signal to the query
    .then(data => {
      setState(data)                              // if everything went well, set the state
    })
    .catch(error => {
      if (error.name === 'AbortError') return     // if the query has been aborted, do nothing
      throw error
    })
  
  return () => {
    abortController.abort()                       // stop the query by aborting on the AbortController on unmount
  }
}, [])

对于不基于Fetch API的异步操作,仍然应该有一种方法来取消这些异步操作,您应该利用这些操作,而不仅仅是检查组件是否被挂载。如果您正在构建自己的API,您可以在其中实现AbortController API来处理它。

更详细地说,检查组件是否被挂载是一个反模式,因为React在内部检查组件是否被挂载以显示警告。再次执行相同的检查只是隐藏警告的一种方法,还有一些更简单的方法来隐藏警告,而不是将这段代码添加到代码库的大部分中。

来源:https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934

其他回答

isMounted方法在大多数情况下是一种反模式,因为它实际上并不清理/取消任何东西,它只是避免更改未挂载组件的状态,但对挂起的异步任务不做任何操作。React团队最近删除了泄漏警告,因为用户不断创建大量反模式来隐藏警告,而不是修复其原因。

但是用纯JS编写可取消的代码确实很棘手。为了解决这个问题,我用自定义钩子制作了自己的库useAsyncEffect2,构建在一个可取消的承诺(c-promise2)之上,用于执行可取消的异步代码以达到其优雅的取消。所有异步阶段(承诺),包括深层的,都是可以取消的。这意味着如果其父上下文被取消,这里的请求将自动中止。当然,可以使用任何其他异步操作来代替请求。

useAsyncEffect演示与普通useState使用(现场演示):

    import React, { useState } from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent({url}) {
      const [text, setText] = useState("");
    
      const cancel = useAsyncEffect(
        function* () {
          setText("fetching...");
          const json = (yield cpAxios(url)).data;
          setText(`Success: ${JSON.stringify(json)}`);
        },
        [url]
      );
    
      return (
        <div>
          <div>{text}</div>
          <button onClick={cancel}>
            Cancel request
          </button>
        </div>
      );
    }

useAsyncEffect演示与内部状态使用(现场演示):

    import React from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent({ url, timeout }) {
      const [cancel, done, result, err] = useAsyncEffect(
        function* () {
          return (yield cpAxios(url).timeout(timeout)).data;
        },
        { states: true, deps: [url] }
      );
    
      return (
        <div>
          {done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
          <button onClick={cancel} disabled={done}>
            Cancel async effect (abort request)
          </button>
        </div>
      );
    }

使用装饰器的类组件(现场演示)

import React, { Component } from "react";
import { ReactComponent } from "c-promise2";
import cpAxios from "cp-axios";

@ReactComponent
class TestComponent extends Component {
  state = {
    text: ""
  };

  *componentDidMount(scope) {
    const { url, timeout } = this.props;
    const response = yield cpAxios(url).timeout(timeout);
    this.setState({ text: JSON.stringify(response.data, null, 2) });
  }

  render() {
    return (<div>{this.state.text}</div>);
  }
}

export default TestComponent;

更多其他例子:

Axios请求的错误处理 用坐标获取天气 Live搜索 暂停和继续 进步捕获

对于这个错误,我有两个解决方案:

返回:

如果你使用了钩子和useEffect,那么使用一个useEffect的返回结束。

useEffect(() => {
    window.addEventListener('mousemove', logMouseMove)
    return () => {
        window.removeEventListener('mousemove', logMouseMove)
    }
}, [])

componentWillUnmount:

如果你使用componentDidMount,那么把componentWillUnmount放在它旁边。

componentDidMount() { 
    window.addEventListener('mousemove', this.logMouseMove)
}

componentWillUnmount() {
    window.removeEventListener('mousemove', this.logMouseMove)
}

移除-不能对卸载的组件警告执行React状态更新,在条件下使用componentDidMount方法,并在componentWillUnmount方法上使该条件为假。例如:-

class Home extends Component {
  _isMounted = false;

  constructor(props) {
    super(props);

    this.state = {
      news: [],
    };
  }

  componentDidMount() {
    this._isMounted = true;

    ajaxVar
      .get('https://domain')
      .then(result => {
        if (this._isMounted) {
          this.setState({
            news: result.data.hits,
          });
        }
      });
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  render() {
    ...
  }
}

为jsx组件添加一个引用,然后检查它是否存在

function Book() {
  const ref = useRef();

  useEffect(() => {
    asyncOperation().then(data => {
      if (ref.current) setState(data);
    })
  });

  return <div ref={ref}>content</div>
}

这是一个React挂钩的具体解决方案

错误

警告:无法对未挂载的组件执行React状态更新。

解决方案

您可以在useEffect内部声明let isMounted = true,一旦组件被卸载,它将在清理回调中被更改。在状态更新之前,你现在有条件地检查这个变量:

useEffect(() => {
  let isMounted = true;               // note mutable flag
  someAsyncOperation().then(data => {
    if (isMounted) setState(data);    // add conditional check
  })
  return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);                               // adjust dependencies to your needs

const Parent = () => { const [mounted, setMounted] = useState(true); return ( <div> Parent: <button onClick={() => setMounted(!mounted)}> {mounted ? "Unmount" : "Mount"} Child </button> {mounted && <Child />} <p> Unmount Child, while it is still loading. It won't set state later on, so no error is triggered. </p> </div> ); }; const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useEffect(() => { let isMounted = true; fetchData(); return () => { isMounted = false; }; // simulate some Web API fetching function fetchData() { setTimeout(() => { // drop "if (isMounted)" to trigger error again // (take IDE, doesn't work with stack snippet) if (isMounted) setState("data fetched") else console.log("aborted setState on unmounted component") }, 4000); } }, []); return <div>Child: {state}</div>; }; ReactDOM.render(<Parent />, document.getElementById("root")); <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>

扩展:自定义useAsync钩子

我们可以将所有样板包封装到一个自定义Hook中,当组件卸载或依赖值之前发生更改时,该Hook会自动中止异步函数:

function useAsync(asyncFn, onSuccess) {
  useEffect(() => {
    let isActive = true;
    asyncFn().then(data => {
      if (isActive) onSuccess(data);
    });
    return () => { isActive = false };
  }, [asyncFn, onSuccess]);
}

// custom Hook for automatic abortion on unmount or dependency change // You might add onFailure for promise errors as well. function useAsync(asyncFn, onSuccess) { useEffect(() => { let isActive = true; asyncFn().then(data => { if (isActive) onSuccess(data) else console.log("aborted setState on unmounted component") }); return () => { isActive = false; }; }, [asyncFn, onSuccess]); } const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useAsync(simulateFetchData, setState); return <div>Child: {state}</div>; }; const Parent = () => { const [mounted, setMounted] = useState(true); return ( <div> Parent: <button onClick={() => setMounted(!mounted)}> {mounted ? "Unmount" : "Mount"} Child </button> {mounted && <Child />} <p> Unmount Child, while it is still loading. It won't set state later on, so no error is triggered. </p> </div> ); }; const simulateFetchData = () => new Promise( resolve => setTimeout(() => resolve("data fetched"), 4000)); ReactDOM.render(<Parent />, document.getElementById("root")); <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>

更多关于效果清理:过度反应:使用效果的完整指南