Infinite loop in useEffect with constant dependency array using react-error-boundary

huangapple go评论59阅读模式
英文:

Infinite loop in useEffect with constant dependency array using react-error-boundary

问题

In my example below, why is there an infinite loop of errors in the devtools console? It seems like resetErrorBoundary() is causing useEffect to trigger again and vice versa leading to an infinite loop, but I don't understand why useEffect would keep running even with a constant value in its dependency array.

This answer solves the problem by explicitly checking for changes to the dependency array value with an if-statement, but shouldn't useEffect do that automatically? I would expect an if-statement like that to be redundant.

这个问题的关键似乎在于,resetErrorBoundary() 导致 useEffect 和它互相触发,从而导致无限循环。但是,我不理解为什么 useEffect 会在其依赖数组中使用常量值时继续运行。

此答案 通过明确检查依赖数组值的更改来解决了这个问题,但 useEffect 应该自动处理这一点,我本来期望像这样的 if 语句是多余的。

https://codesandbox.io/p/github/adamerose/error-boundary-example/main?file=%2Fsrc%2FApp.jsx

注意 - 这只是一个最小的示例。我的实际项目中,依赖数组中包含 location.pathname,因为我希望在 URL 导航时重置错误,但我发现无论我在依赖数组中放什么,都会导致无限循环。

英文:

In my example below, why is there an infinite loop of errors in the devtools console? It seems like resetErrorBoundary() is causing useEffect to trigger again and vice versa leading to an infinite loop, but I don't understand why useEffect would keep running even with a constant value in its dependency array.

This answer solves the problem by explicitly checking for changes to the dependency array value with an if-statement, but shouldn't useEffect do that automatically? I would expect an if-statement like that to be redundant.

https://codesandbox.io/p/github/adamerose/error-boundary-example/main?file=%2Fsrc%2FApp.jsx

import { useEffect } from "react";
import { ErrorBoundary } from "react-error-boundary";

function ThisComponentWillError() {
  throw Error("SomeError");
}

function App() {
  return (
    <main>
      <StandardErrorBoundary>
        <ThisComponentWillError />
      </StandardErrorBoundary>
    </main>
  );
}

function ErrorFallback({ error, resetErrorBoundary }) {
  useEffect(() => {
    resetErrorBoundary();
  }, ["CONSTANT"]);

  return (
    <div>
      <p>Something went wrong:</p>
      <pre>{error.toString()}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function StandardErrorBoundary({ children }) {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>{children}</ErrorBoundary>
  );
}

export default App;

Note - This is just a minimal example. My actual project has location.pathname in the dependency because I want to reset errors on URL navigation, but I realized no matter what I had in the dependency array it would infinitely loop.

答案1

得分: 1

以下是已经翻译好的内容:

"One way to debug something like this is to inspect the source code of the library being used. Thankfully react-error-boundary is just one component, it was relatively easier to inspect.

You are assuming that the ErrorFallback component is re-rendered when resetErrorBoundary is called. Instead it is completely remounted. Once remounted all effects will run again, cause it is like a new first invocation of the function.

Here is the source code. I have commented the irrelevant part:

const initialState: ErrorBoundaryState = {error: null}

class ErrorBoundary extends React.Component<
  React.PropsWithRef<React.PropsWithChildren<ErrorBoundaryProps>>,
  ErrorBoundaryState
> {
  static getDerivedStateFromError(error: Error) {
    return {error}
  }

  state = initialState
  resetErrorBoundary = (...args: Array<unknown>) => {
    this.props.onReset?.(...args)
    this.reset()
  }

  reset() {
    this.setState(initialState)
  }

  ....

  ....

  render() {
    const {error} = this state

    const {fallbackRender, FallbackComponent, fallback} = this.props

    if (error !== null) {
      const props = {
        error,
        resetErrorBoundary: this.resetErrorBoundary,
      }
      if (React.isValidElement(fallback)) {
        return fallback
      } else if (typeof fallbackRender is 'function') {
        return fallbackRender(props)
      } else if (FallbackComponent) {
        return <FallbackComponent {...props} />
      } else {
        throw new Error(
          'react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop',
        )
      }
    }

    return this.props.children
  }
}

So once resetErrorBoundary is called. The state is re-initialized and becomes {error:null}. Now in that case the children of the error boundary wrapped code will be rendered instead of the fallback. In the above case, the child tree again throws an error and hence state becomes something other than {error:null}. The render method of ErrorBoundary is called again and this time the Fallback component is rendered because this time this.state.error` is not null. Hence completing the loop and this goes on.

PS: I was able to find out that the component was getting remounted by running a useEffect with empty dependency."

英文:

One way to debug something like this is to inspect the source code of the library being. used. Thankfully react-error-boundary is just one component, it was relatively easier to inspect.

You are assuming that the ErrorFallback component is re-rendered when resetErrorBoundary is called. Instead it is completely remounted. Once remounted all effects will run again, cause it is like a new first invocation of the function.

Here is the source code. I have commented the irrelevant part:

const initialState: ErrorBoundaryState = {error: null}

class ErrorBoundary extends React.Component&lt;
  React.PropsWithRef&lt;React.PropsWithChildren&lt;ErrorBoundaryProps&gt;&gt;,
  ErrorBoundaryState
&gt; {
  static getDerivedStateFromError(error: Error) {
    return {error}
  }

  state = initialState
  resetErrorBoundary = (...args: Array&lt;unknown&gt;) =&gt; {
    this.props.onReset?.(...args)
    this.reset()
  }

  reset() {
    this.setState(initialState)
  }

  ....

  ....

  render() {
    const {error} = this.state

    const {fallbackRender, FallbackComponent, fallback} = this.props

    if (error !== null) {
      const props = {
        error,
        resetErrorBoundary: this.resetErrorBoundary,
      }
      if (React.isValidElement(fallback)) {
        return fallback
      } else if (typeof fallbackRender === &#39;function&#39;) {
        return fallbackRender(props)
      } else if (FallbackComponent) {
        return &lt;FallbackComponent {...props} /&gt;
      } else {
        throw new Error(
          &#39;react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop&#39;,
        )
      }
    }

    return this.props.children
  }
}

So once resetErrorBoundary is called. The state is re-initialized and becomes {error:null}. Now in that case the children of the error boundary wrapped code will be rendered instead of the fallback. In the above case, the child tree again throws an error and hence state becomes something other than {error:null}. The render method of ErrorBoundary is called again and this time the Fallback component is rendered because this time this.state.error` is not null. Hence completing the loop and this goes on.

PS: I was able to find out that the component was getting remounted by running a useEffect with empty dependency.

huangapple
  • 本文由 发表于 2023年3月21日 03:02:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/75794295.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定