无法对未安装的组件执行 React 状态更新

新手上路,请多包涵

问题

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

我非常仔细地查看了我的代码并尝试放置一些保护条款,但问题仍然存在,我仍在观察警告。

因此,我有两个问题:

  1. 我如何从堆栈跟踪中找出 哪个特定组件和事件处理程序或生命周期挂钩对规则违规负责?

  2. 好吧,如何解决问题本身,因为我的代码是在考虑到这个陷阱的情况下编写的,并且已经在尝试防止它,但是一些底层组件仍在生成警告。

浏览器控制台

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

在此处输入图像描述

代码

书.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;

原文由 Igor Soloydenko 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 913
2 个回答

这是一个 React Hooks 特定的解决方案

错误

警告:无法对未安装的组件执行 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>

有关效果清理的更多信息: 过度反应:useEffect 的完整指南

原文由 ford04 发布,翻译遵循 CC BY-SA 4.0 许可协议

要删除 - 无法对未安装的组件警告执行 React 状态更新,请在条件下使用 componentDidMount 方法,并在 componentWillUnmount 方法上使该条件为 false。例如 : -

 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() {
    ...
  }
}

原文由 vinod 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题