无法在已卸载的组件上执行React状态更新

Igo*_*nko 28 javascript setstate typescript lodash reactjs

问题

我正在React中编写一个应用程序,无法避免出现一个超级常见的陷阱,该陷阱正在调用setState(...)after 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
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

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;
Run Code Online (Sandbox Code Playgroud)

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>
  );
}
Run Code Online (Sandbox Code Playgroud)

更新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;
Run Code Online (Sandbox Code Playgroud)

for*_*d04 264

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

错误

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

解决方案

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

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)
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"));
Run Code Online (Sandbox Code Playgroud)

扩展:自定义useAsync挂钩

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

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

<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>
Run Code Online (Sandbox Code Playgroud)
function useAsync(asyncFn, onSuccess) {
  useEffect(() => {
    let isActive = true;
    asyncFn().then(data => {
      if (isActive) onSuccess(data);
    });
    return () => { isActive = false };
  }, [asyncFn, onSuccess]);
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我们在这里利用内置的效果 [cleanup](https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1) 功能,该功能在依赖项更改时以及在任何情况下组件发生变化时运行卸载。因此,这是将“isMounted”标志切换为“false”的完美位置,可以从周围的效果回调闭包范围访问该标志。您可以将清理功能视为其相应的效果。 (7认同)
  • 你的伎俩奏效了!我想知道背后的魔力是什么? (4认同)
  • /sf/answers/4424957351/ 和 https://medium.com/better-programming/why-cant-my-component-unmount-in-react-fd2c13cd58f4 很有趣,但最终你的答案才是最终有帮助的我让我的工作。谢谢! (3认同)
  • 如果您已经缩小了导致问题的原因/使用效果的范围,那么这似乎是一个不错的解决方案,但其中一个问题 1. 询问如何找出哪个组件、处理程序、挂钩等负责此错误。为什么一个问题有这么多的赞成票,却没有提及整个问题并回答这两个问题?我检查过,整个线程中也没有任何关于查找错误根源的提示的帖子;根据堆栈跟踪无法弄清楚吗? (3认同)
  • @Woodz 是的,很好的提示。`useCallback` 是 React 中常用且推荐的方式,用于将依赖责任推迟到 `useAsync` 的客户端。您可以切换到“useAsync”内的可变引用来存储最近的回调,以便客户端可以直接传递其函数/回调而无需依赖。但我会谨慎使用这种模式,因为这可能更令人困惑和势在必行。 (3认同)

May*_*bit 70

如果上述解决方案不起作用,试试这个,它对我有用:

componentWillUnmount() {
    // fix Warning: Can't perform a React state update on an unmounted component
    this.setState = (state,callback)=>{
        return;
    };
}
Run Code Online (Sandbox Code Playgroud)

  • 我不推荐这个解决方案,它非常hacky。@BadriPaudel 这将用一个不执行任何操作的函数替换 componentWillUnmount 之后的 setState 函数。setState函数将继续被调用。 (4认同)
  • @BadriPaudel 当转义组件时返回 null,它将不再在内存中保存任何数据 (2认同)
  • 返回什么?就这样粘贴吗? (2认同)

sfl*_*che 26

有一个很常见的钩子可以useIsMounted解决这个问题(对于功能组件)......

import { useRef, useEffect } from 'react';

export function useIsMounted() {
  const isMounted = useRef(false);

  useEffect(() => {
    isMounted.current = true;
    return () => isMounted.current = false;
  }, []);

  return isMounted;
}
Run Code Online (Sandbox Code Playgroud)

然后在您的功能组件中

function Book() {
  const isMounted = useIsMounted();
  ...

  useEffect(() => {
    asyncOperation().then(data => {
      if (isMounted.current) { setState(data); }
    })
  });
  ...
}
Run Code Online (Sandbox Code Playgroud)

  • @AyushKumar:是的,你可以!这就是钩子的美妙之处!`isMounted` 状态将特定于调用 `useIsMounted` 的每个组件! (5认同)

小智 22

根据 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
  }
}, [])
Run Code Online (Sandbox Code Playgroud)

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

对于更多上下文,检查组件是否已安装是一种反模式,因为React 会在内部检查组件是否已安装以显示该警告。再次进行相同的检查只是隐藏警告的一种方法,并且有一些比在代码库的很大一部分上添加这段代码更简单的方法来隐藏它们。

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


vin*_*nod 15

To remove - Can't perform a React state update on an unmounted component warning, use componentDidMount method under a condition and make false that condition on componentWillUnmount method. For example : -

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() {
    ...
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 对于钩子组件,请使用:`const isMountedComponent = useRef(true); useEffect(() =&gt; { if (isMountedComponent.current) { ... } return () =&gt; { isMountedComponent.current = false; }; });` (11认同)
  • 这行得通,但是为什么这行得通呢?究竟是什么导致了这个错误?以及如何修复它:| (4认同)
  • 效果很好。它停止了 setState 方法的重复调用,因为它在 setState 调用之前验证 _isMounted 值,然后最后在 componentWillUnmount() 中再次重置为 false。我想,这就是它的运作方式。 (2认同)
  • @Abhinav我最好的猜测为什么它有效是`_isMounted`不是由React管理的(与`state`不同),因此不受React的[渲染管道](https://reactjs.org/docs/react-component .html#setstate)。问题是,当一个组件被设置为卸载时,React 会使任何对“setState()”的调用出队(这将触发“重新渲染”);因此,状态永远不会更新 (2认同)

Pet*_*erg 12

我收到这个警告可能是因为setState从效果挂钩调用(这在这 3 个链接在一起的问题中 讨论过)。

无论如何,升级反应版本消除了警告。


scr*_*2em 10

React 已经删除了这个警告,但这里有一个更好的解决方案(不仅仅是解决方法)

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() 
  }
}, [])
Run Code Online (Sandbox Code Playgroud)


Dan*_*ana 8

@ford04 的解决方案对我来说不起作用,特别是如果您需要在多个地方使用 isMounted (例如多个 useEffect ),建议使用 useRef ,如下所示:

  1. 必备套餐
"dependencies": 
{
  "react": "17.0.1",
}
"devDependencies": { 
  "typescript": "4.1.5",
}

Run Code Online (Sandbox Code Playgroud)
  1. 我的钩子组件
export const SubscriptionsView: React.FC = () => {
  const [data, setData] = useState<Subscription[]>();
  const isMounted = React.useRef(true);

  React.useEffect(() => {
    if (isMounted.current) {
      // fetch data
      // setData (fetch result)

      return () => {
        isMounted.current = false;
      };
    }
  }
});
Run Code Online (Sandbox Code Playgroud)


ic3*_*3rg 5

尝试更改setDivSizeThrottleable

this.setDivSizeThrottleable = throttle(
  () => {
    if (this.isComponentMounted) {
      this.setState({
        pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
      });
    }
  },
  500,
  { leading: false, trailing: true }
);
Run Code Online (Sandbox Code Playgroud)


小智 5

我知道您没有使用历史记录,但在我的情况下,我使用了useHistoryReact Router DOM 中的钩子,它在状态保留在我的 React Context Provider 中之前卸载了组件。

为了解决这个问题,我使用了withRouter嵌套组件的钩子,在我的例子中export default withRouter(Login),在组件内部const Login = props => { ...; props.history.push("/dashboard"); ...。我还props.history.push从组件中删除了另一个,例如,if(authorization.token) return props.history.push('/dashboard')因为这会导致循环,因为authorization状态。

将新项目推送到history的替代方法。


Sco*_*ger 5

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

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

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

  return <div ref={ref}>content</div>
}
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

32188 次

最近记录:

5 年,10 月 前