问题

我正在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;

当前回答

更新不要使用我原来的答案,因为它不起作用

这个答案是基于可取消承诺的使用和makeccancable中的一个注释,我迁移到使用钩子。然而,它似乎不能取消async/await链,甚至cancel -promise也不支持取消await链

对此做了更多的研究,似乎是一些内部的谷歌原因阻止了可取消承诺进入标准。

此外,“蓝鸟”有一些承诺,引入了可取消的承诺,但它在世博会上不起作用,至少我还没有看到它在世博会上起作用的例子。

公认的答案是最好的。因为我使用TypeScript,我已经适应了一些修改的代码(我显式设置依赖,因为接受的答案的隐式依赖似乎在我的应用程序上重新呈现循环,添加并使用async/await而不是承诺链,传递一个引用到挂载对象,以便async/await链可以在需要时提前取消)

/**
 * This starts an async function and executes another function that performs
 * React state changes if the component is still mounted after the async
 * operation completes
 * @template T
 * @param {(mountedRef: React.MutableRefObject<boolean>) => Promise<T>} asyncFunction async function,
 *   it has a copy of the mounted ref so an await chain can be canceled earlier.
 * @param {(asyncResult: T) => void} onSuccess this gets executed after async
 *   function is resolved and the component is still mounted
 * @param {import("react").DependencyList} deps
 */
export function useAsyncSetEffect(asyncFunction, onSuccess, deps) {
  const mountedRef = useRef(false);
  useEffect(() => {
    mountedRef.current = true;
    (async () => {
      const x = await asyncFunction(mountedRef);
      if (mountedRef.current) {
        onSuccess(x);
      }
    })();
    return () => {
      mountedRef.current = false;
    };
  }, deps);
}

原来的答案

由于我有许多不同的异步操作,所以我使用cancelable-promise包以最小的代码更改来解决这个问题。

以前的代码:

useEffect(() => 
  (async () => {
    const bar = await fooAsync();
    setSomeState(bar);
  })(),
  []
);

新代码:

import { cancelable } from "cancelable-promise";

...

useEffect(
  () => {
    const cancelablePromise = cancelable(async () => {
      const bar = await fooAsync();
      setSomeState(bar);
    })
    return () => cancelablePromise.cancel();
  },
  []
);

你也可以像这样在一个自定义实用函数中编写它

/**
 * This wraps an async function in a cancelable promise
 * @param {() => PromiseLike<void>} asyncFunction
 * @param {React.DependencyList} deps
 */
export function useCancelableEffect(asyncFunction, deps) {
  useEffect(() => {
    const cancelablePromise = cancelable(asyncFunction());
    return () => cancelablePromise.cancel();
  }, deps);
}

其他回答

如果您正在从axios中获取数据,而错误仍然发生,只需将setter包装在条件中

let isRendered = useRef(false);
useEffect(() => {
    isRendered = true;
    axios
        .get("/sample/api")
        .then(res => {
            if (isRendered) {
                setState(res.data);
            }
            return null;
        })
        .catch(err => console.log(err));
    return () => {
        isRendered = false;
    };
}, []);

根据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

受@ford04接受的答案的启发,我有更好的方法来处理它,而不是在useAsync内部使用useEffect创建一个新函数,返回componentWillUnmount的回调:

function asyncRequest(asyncRequest, onSuccess, onError, onComplete) {
  let isMounted=true
  asyncRequest().then((data => isMounted ? onSuccess(data):null)).catch(onError).finally(onComplete)
  return () => {isMounted=false}
}

...

useEffect(()=>{
        return asyncRequest(()=>someAsyncTask(arg), response=> {
            setSomeState(response)
        },onError, onComplete)
    },[])

更新不要使用我原来的答案,因为它不起作用

这个答案是基于可取消承诺的使用和makeccancable中的一个注释,我迁移到使用钩子。然而,它似乎不能取消async/await链,甚至cancel -promise也不支持取消await链

对此做了更多的研究,似乎是一些内部的谷歌原因阻止了可取消承诺进入标准。

此外,“蓝鸟”有一些承诺,引入了可取消的承诺,但它在世博会上不起作用,至少我还没有看到它在世博会上起作用的例子。

公认的答案是最好的。因为我使用TypeScript,我已经适应了一些修改的代码(我显式设置依赖,因为接受的答案的隐式依赖似乎在我的应用程序上重新呈现循环,添加并使用async/await而不是承诺链,传递一个引用到挂载对象,以便async/await链可以在需要时提前取消)

/**
 * This starts an async function and executes another function that performs
 * React state changes if the component is still mounted after the async
 * operation completes
 * @template T
 * @param {(mountedRef: React.MutableRefObject<boolean>) => Promise<T>} asyncFunction async function,
 *   it has a copy of the mounted ref so an await chain can be canceled earlier.
 * @param {(asyncResult: T) => void} onSuccess this gets executed after async
 *   function is resolved and the component is still mounted
 * @param {import("react").DependencyList} deps
 */
export function useAsyncSetEffect(asyncFunction, onSuccess, deps) {
  const mountedRef = useRef(false);
  useEffect(() => {
    mountedRef.current = true;
    (async () => {
      const x = await asyncFunction(mountedRef);
      if (mountedRef.current) {
        onSuccess(x);
      }
    })();
    return () => {
      mountedRef.current = false;
    };
  }, deps);
}

原来的答案

由于我有许多不同的异步操作,所以我使用cancelable-promise包以最小的代码更改来解决这个问题。

以前的代码:

useEffect(() => 
  (async () => {
    const bar = await fooAsync();
    setSomeState(bar);
  })(),
  []
);

新代码:

import { cancelable } from "cancelable-promise";

...

useEffect(
  () => {
    const cancelablePromise = cancelable(async () => {
      const bar = await fooAsync();
      setSomeState(bar);
    })
    return () => cancelablePromise.cancel();
  },
  []
);

你也可以像这样在一个自定义实用函数中编写它

/**
 * This wraps an async function in a cancelable promise
 * @param {() => PromiseLike<void>} asyncFunction
 * @param {React.DependencyList} deps
 */
export function useCancelableEffect(asyncFunction, deps) {
  useEffect(() => {
    const cancelablePromise = cancelable(asyncFunction());
    return () => cancelablePromise.cancel();
  }, deps);
}
const handleClick = async (item: NavheadersType, index: number) => {
    const newNavHeaders = [...navheaders];
    if (item.url) {
      await router.push(item.url);   =>>>> line causing error (causing route to happen)
      // router.push(item.url);  =>>> coreect line
      newNavHeaders.forEach((item) => (item.active = false));
      newNavHeaders[index].active = true;
      setnavheaders([...newNavHeaders]);
    }
  };