【问题标题】:Can't perform a React state update on an unmounted component无法对未安装的组件执行 React 状态更新
【发布时间】:2021-05-30 05:43:13
【问题描述】:

问题

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

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

因此,我有两个问题:

  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

代码

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>
  );
}

更新一:取消节流功能(还是不行)

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;

【问题讨论】:

  • 如果您注释掉添加和删除侦听器,问题是否仍然存在?
  • @ic3b3rg 如果没有事件监听代码,问题就会消失
  • 好的,您是否尝试过使用this.setDivSizeThrottleable.cancel() 而不是this.isComponentMounted 守卫的建议?
  • @ic3b3rg 还是一样的运行时警告。

标签: javascript reactjs typescript lodash setstate


【解决方案1】:

我有两个解决这个错误的方法:

  1. 返回

如果你使用hookuseEffect,那么在useEffect后面加上一个return

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

如果你用的是componentDidMount,那么就把componentWillUnmount放在旁边吧。

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

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

【讨论】:

    【解决方案2】:

    “如果你的 useEffect 中有 setter hook,返回一个清理函数” - me

    检查你的 useEffect() 函数,它有一个 setter 钩子 setSomething(),并让它在它的末尾返回一个清理函数。例如:

    useEffect(() => {
      ...
      setSomething(something); //
      ...
      return () => { }; // The cleanup function we have to add, it could be empty like this
    }, [/*whatever deps or no deps at all*/]);  
    

    【讨论】:

    • 他已经提到他尝试过这个。
    【解决方案3】:

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

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

    • useAsyncEffect Demo 与普通 useState 用法 (Live Demo):
        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 使用内部状态的演示 (Live Demo):
        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;
    

    更多其他示例:

    【讨论】:

      【解决方案4】:

      如果你是从 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;
          };
      }, []);
      

      【讨论】:

      • 为什么不使用 isRendered 而不使用 .current ?这是一个功能吗?
      • @usertest 我刚刚实现了这个解决方案,但必须使用 .current。
      【解决方案5】:

      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() 
        }
      }, [])
      

      【讨论】:

        【解决方案6】:

        检查组件是否已挂载实际上是一种反模式as per React documentationsetState 警告的解决方案是利用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

        【讨论】:

          【解决方案7】:

          给一个 jsx 组件添加一个 ref,然后检查它是否存在

          function Book() {
            const ref = useRef();
          
            useEffect(() => {
              asyncOperation().then(data => {
                if (ref.current) setState(data);
              })
            });
          
            return <div ref={ref}>content</div>
          }
          

          【讨论】:

          • 这对我有用,Nextjs 应用程序
          【解决方案8】:

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

          错误

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

          解决方案

          您可以在useEffect 中声明let isMounted = true,一旦卸载组件,cleanup callback 中的内容就会更改。在状态更新之前,您现在有条件地检查此变量:

          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 中,如果组件卸载或依赖值之前发生变化,它会自动中止异步函数:

          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>

          有关效果清理的更多信息:Overreacted: A Complete Guide to useEffect

          【讨论】:

          • 你的把戏奏效了!我想知道背后的魔力是什么?
          • 我们在这里利用了内置效果 cleanup 功能,当依赖关系发生变化时以及在任何一种情况下组件卸载时都会运行。所以这是将isMounted 标志切换为false 的理想场所,可以从周围的效果回调闭包范围访问。你可以把清理函数想象成belonging to对应的效果。
          • 有道理!我对你的回答很满意。我从中学到了。
          • stackoverflow.com/a/63213676medium.com/better-programming/… 很有趣,但最终你的回答最终帮助我开始工作。谢谢!
          • @Woodz 是的,很好的提示。 useCallback 是 React 中常用且推荐的方式,用于将依赖关系的责任交给 useAsync 的客户端。您可以切换到 useAsync 内的可变引用来存储最近的回调,因此客户端可以直接传递他们的函数/回调而无需依赖。但我会谨慎使用这种模式,因为它可能更令人困惑和命令式的方法。
          【解决方案9】:

          我通过提供 useEffect 挂钩中使用的所有参数解决了这个问题

          代码报错:

          useEffect(() => {
              getDistrict({
                geonameid: countryId,
                subdistrict: level,
              }).then((res) => {
                ......
              });
            }, [countryId]);
          

          修复后的代码:

          useEffect(() => {
              getDistrict({
                geonameid: countryId,
                subdistrict: level,
              }).then((res) => {
                ......
              });
            }, [countryId,level]);
          

          可以看到,在我提供了所有应该通过的参数(包括关卡参数)后,问题就解决了。

          【讨论】:

            【解决方案10】:

            这是一个简单的解决方案。这个警告是由于当我们在后台执行一些获取请求时(因为一些请求需要一些时间。)并且我们从那个屏幕导航回来,然后他们反应不能更新状态。这是示例代码。 在每个状态更新之前写下这一行。

            if(!isScreenMounted.current) return;
            

            这是完整的代码

            import React , {useRef} from 'react'
            import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
            import BASEURL from '../constants/BaseURL';
            const SearchScreen = () => {
                const isScreenMounted = useRef(true)
                useEffect(() => {
                    return () =>  isScreenMounted.current = false
                },[])
            
                const ConvertFileSubmit = () => {
                    if(!isScreenMounted.current) return;
                     setUpLoading(true)
             
                     var formdata = new FormData();
                     var file = {
                         uri: `file://${route.params.selectedfiles[0].uri}`,
                         type:`${route.params.selectedfiles[0].minetype}`,
                         name:`${route.params.selectedfiles[0].displayname}`,
                     };
                     
                     formdata.append("file",file);
                     
                     fetch(`${BASEURL}/UploadFile`, {
                         method: 'POST',
                         body: formdata,
                         redirect: 'manual'
                     }).then(response => response.json())
                     .then(result => {
                         if(!isScreenMounted.current) return;
                         setUpLoading(false)    
                     }).catch(error => {
                         console.log('error', error)
                     });
                 }
            
                return(
                <>
                    <StatusBar barStyle="dark-content" />
                    <SafeAreaView>
                        <ScrollView
                        contentInsetAdjustmentBehavior="automatic"
                        style={styles.scrollView}>
                           <Text>Search Screen</Text>
                        </ScrollView>
                    </SafeAreaView>
                </>
                )
            }
            
            export default SearchScreen;
            
            
            const styles = StyleSheet.create({
                scrollView: {
                    backgroundColor:"red",
                },
                container:{
                    flex:1,
                    justifyContent:"center",
                    alignItems:"center"
                }
            })
            

            【讨论】:

              【解决方案11】:

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

              这个答案是基于使用可取消的承诺和makecancelable 中的一个注释,我迁移到使用钩子。但是,它似乎并没有取消 async/await 甚至 cancelable-promise does not support canceling of a chain of awaits 的链

              对此进行更多研究,似乎some internal Google reasons prevented cancelable promises from coming into the standard

              此外,Bluebird 有一些承诺,它引入了可取消的承诺,但它在 Expo 中不起作用,或者至少我还没有看到它在 Expo 中起作用的示例。

              accepted answer 是最好的。由于我使用 TypeScript,我对代码进行了一些修改(我明确设置了依赖项,因为接受的答案的隐式依赖项似乎在我的应用程序上提供了重新渲染循环,添加并使用 async/await 而不是 promise 链,传递一个引用挂载的对象,以便在需要时可以提前取消异步/等待链)

              /**
               * 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);
              }
              

              原答案

              由于我有许多不同的操作 async,因此我使用 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);
              }
              

              【讨论】:

                【解决方案12】:

                最简单和最紧凑的解决方案(带有解释)在下面被视为单线解决方案。

                useEffect(() => { return () => {}; }, []);
                

                上面的 useEffect() 示例返回一个回调函数,触发 React 在更新状态之前完成其生命周期的卸载部分。

                只需要这个非常简单的解决方案。此外,它也不同于 @ford04 和 @sfletche 提供的虚构语法。顺便说一句,@ford04 中的以下代码 sn-p 纯属虚构语法(@sfletche@vinod@guneetgstar@Drew Cordano 使用了完全相同的虚构语法)。

                数据 => {      

                someAsyncOperation().then(data => {
                    if (isMounted) setState(data);    // add conditional check
                  })
                

                我所有的 linter 和我整个团队的所有 linter 都不会接受它,他们会报告 Uncaught SyntaxError: unexpected token: '=&gt;'。我很惊讶没有人掌握想象的语法。参与过这个问题的人,尤其是支持投票的人,能向我解释他们是如何找到适合他们的解决方案的吗?

                【讨论】:

                • 您关于“虚构语法”的说法是错误的。它适用于许多其他人!如果您的someAsynchronousOperation() 返回值类型是Promise&lt;void&gt;,那么肯定data 会导致TypeScript 编译错误。但是,如果是Promise&lt;X&gt;,而X 不是undefined/void/never,你肯定可以使用.then(data =&gt; {...})!您还没有提供一个完整的最小示例来推理它。如果您希望解决您的特定代码问题,请在 StackOverflow 上打开一个单独的问题。您不想被否决或标记答案。
                • 你说的是箭头函数吗?这是在 ES6 中引入的。我不知道您是如何配置 linter 的,但这是非常常见的语法。 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
                • 好吧,我没有对你投反对票...在不了解您的 linter 设置或技术堆栈的任何信息的情况下,我无法确定为什么这对您来说显示为语法错误,但可能是因为您的 linter 使用的是不支持此功能的旧版本 Javascript 的设置,但这只是猜测。
                【解决方案13】:

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

                1. 基本包
                "dependencies": 
                {
                  "react": "17.0.1",
                }
                "devDependencies": { 
                  "typescript": "4.1.5",
                }
                
                
                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;
                      };
                    }
                  }
                });
                

                【讨论】:

                • 同意这一点,使用此解决方案更方便,并且确保单一事实来源。
                【解决方案14】:
                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]);
                    }
                  };
                

                【讨论】:

                • 你能补充一些定义吗?
                【解决方案15】:

                受@ford04 回答的启发,我使用了这个钩子,它还接受成功、错误、finally 和 abortFn 的回调:

                export const useAsync = (
                        asyncFn, 
                        onSuccess = false, 
                        onError = false, 
                        onFinally = false, 
                        abortFn = false
                    ) => {
                
                    useEffect(() => {
                        let isMounted = true;
                        const run = async () => {
                            try{
                                let data = await asyncFn()
                                if (isMounted && onSuccess) onSuccess(data)
                            } catch(error) {
                                if (isMounted && onError) onSuccess(error)
                            } finally {
                                if (isMounted && onFinally) onFinally()
                            }
                        }
                        run()
                        return () => {
                            if(abortFn) abortFn()
                            isMounted = false
                        };
                    }, [asyncFn, onSuccess])
                }
                

                如果 asyncFn 正在从后端进行某种获取,则在卸载组件时中止它通常是有意义的(但并非总是如此,有时如果您将一些数据加载到存储中,您也可以这样做即使组件已卸载,也只想完成它)

                【讨论】:

                  【解决方案16】:

                  受到@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)
                      },[])
                  
                  

                  【讨论】:

                  • 我不建议依赖本地的 isMounted 变量,而是让它成为一个状态(通过 useState 钩子)。
                  • 这有什么关系?至少我想不出任何不同的行为。
                  【解决方案17】:

                  有一个相当常见的钩子叫做useIsMounted,它解决了这个问题(对于功能组件)......

                  import { useRef, useEffect } from 'react';
                  
                  export function useIsMounted() {
                    const isMounted = useRef(false);
                  
                    useEffect(() => {
                      isMounted.current = true;
                      return () => isMounted.current = false;
                    }, []);
                  
                    return isMounted;
                  }
                  

                  然后在你的功能组件中

                  function Book() {
                    const isMounted = useIsMounted();
                    ...
                  
                    useEffect(() => {
                      asyncOperation().then(data => {
                        if (isMounted.current) { setState(data); }
                      })
                    });
                    ...
                  }
                  

                  【讨论】:

                  • 我们可以在多个组件中使用同一个钩子吗?
                  • @AyushKumar:是的,你可以!这就是钩子的美妙之处! isMounted 状态将特定于每个调用 useIsMounted! 的组件
                  • 我想useIsMounted这种解决方式应该包含在核心包中。
                  • 另一个问题是我是否在我的 useEffect 挂钩中添加了 UseIsMounted,并且我已经启动了一个 listener。在代码中添加return () =&gt; 会导致任何泄漏吗?
                  【解决方案18】:

                  根据您打开网页的方式,您可能不会导致安装。比如使用&lt;Link/&gt;返回一个已经挂载在虚拟DOM中的页面,所以需要一个componentDidMount生命周期的数据就会被捕获。

                  【讨论】:

                  • 你是说componentDidMount() 可以被调用两次而没有中间的componentWillUnmount() 调用?我认为这是不可能的。
                  • 不,我是说它没有被调用两次,这就是为什么在使用&lt;Link/&gt; 时页面不处理componentDidMount() 内的代码。我使用 Redux 来解决这些问题,并将网页的数据保存在 Reducer 存储中,这样我就不需要重新加载页面了。
                  【解决方案19】:

                  基于@ford04 的回答,这里同样封装在一个方法中:

                  import React, { FC, useState, useEffect, DependencyList } from 'react';
                  
                  export function useEffectAsync( effectAsyncFun : ( isMounted: () => boolean ) => unknown, deps?: DependencyList ) {
                      useEffect( () => {
                          let isMounted = true;
                          const _unused = effectAsyncFun( () => isMounted );
                          return () => { isMounted = false; };
                      }, deps );
                  } 
                  

                  用法:

                  const MyComponent : FC<{}> = (props) => {
                      const [ asyncProp , setAsyncProp ] = useState( '' ) ;
                      useEffectAsync( async ( isMounted ) =>
                      {
                          const someAsyncProp = await ... ;
                          if ( isMounted() )
                               setAsyncProp( someAsyncProp ) ;
                      });
                      return <div> ... ;
                  } ;
                  

                  【讨论】:

                    【解决方案20】:

                    我遇到了类似的问题,感谢 @ford04 帮助我。

                    但是,发生了另一个错误。

                    注意。我正在使用 ReactJS 钩子

                    ndex.js:1 Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
                    

                    导致错误的原因是什么?

                    import {useHistory} from 'react-router-dom'
                    
                    const History = useHistory()
                    if (true) {
                      history.push('/new-route');
                    }
                    return (
                      <>
                        <render component />
                      </>
                    )
                    

                    这行不通,因为尽管您正在重定向到新页面,但所有状态和道具都在 dom 上进行操作,或者只是渲染到上一页并没有停止。

                    我找到了什么解决方案

                    import {Redirect} from 'react-router-dom'
                    
                    if (true) {
                      return <redirect to="/new-route" />
                    }
                    return (
                      <>
                        <render component />
                      </>
                    )
                    

                    【讨论】:

                    • 不错的解决方案,但应该返回
                    【解决方案21】:

                    我遇到了类似的问题并解决了:

                    我通过在 redux 上发送一个操作来自动让用户登录 (将身份验证令牌置于 redux 状态)

                    然后我试图用 this.setState({succ_message: "...") 显示一条消息 在我的组件中。

                    组件在控制台上看起来是空的,但出现相同的错误:“未安装的组件”..“内存泄漏”等。

                    在我阅读了沃尔特在这个帖子中的回答之后

                    我注意到在我的应用程序的路由表中, 如果用户已登录,我的组件的路由无效:

                    {!this.props.user.token &&
                            <div>
                                <Route path="/register/:type" exact component={MyComp} />                                             
                            </div>
                    }
                    

                    无论令牌是否存在,我都让 Route 可见。

                    【讨论】:

                      【解决方案22】:

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

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

                      【讨论】:

                      • @BadriPaudel 转义组件时返回 null,它将不再在内存中保存任何数据
                      • 返回什么?照原样粘贴?
                      • 你节省了我的时间。非常感谢。没有它就无法通过 React 测试。
                      • 我不推荐这个解决方案,它很hacky。 @BadriPaudel 这将在 componentWillUnmount 之后用一个什么都不做的函数替换 setState 函数。 setState 函数将继续被调用。
                      【解决方案23】:

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

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

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

                      【讨论】:

                        【解决方案24】:

                        我收到此警告可能是因为从效果挂钩调用 setState(这在这 3 个 issues linked together 中进行了讨论)。

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

                        【讨论】:

                          【解决方案25】:

                          要删除 - 无法对未安装的组件警告执行 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() {
                              ...
                            }
                          }
                          

                          【讨论】:

                          • 这行得通,但为什么要行得通?究竟是什么导致了这个错误?以及这是如何解决的:|
                          • 它工作正常。它停止了 setState 方法的重复调用,因为它在 setState 调用之前验证 _isMounted 值,然后最后在 componentWillUnmount() 中再次重置为 false。我想,这就是它的工作方式。
                          • 钩子组件使用这个:const isMountedComponent = useRef(true); useEffect(() =&gt; { if (isMountedComponent.current) { ... } return () =&gt; { isMountedComponent.current = false; }; });
                          • @x-magix 你真的不需要 ref,你可以使用一个局部变量,返回函数可以关闭。
                          • @Abhinav 我最好的猜测是,_isMounted 不是由 React 管理的(与 state 不同),因此不受 React 的 rendering pipeline 的约束。问题是,当一个组件被设置为卸载时,React 会取消对setState() 的任何调用(这将触发“重新渲染”);因此,状态永远不会更新
                          【解决方案26】:

                          尝试将setDivSizeThrottleable 更改为

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

                          【讨论】:

                          • 我试过了。现在我一直看到警告,我只是在进行此更改之前不时地观察调整窗口大小。 ¯_(ツ)_/¯ 感谢您尝试这个。
                          【解决方案27】:

                          编辑:我刚刚意识到警告引用了一个名为 TextLayerInternal 的组件。这很可能是你的错误所在。其余部分仍然相关,但可能无法解决您的问题。

                          1) 获取此警告的组件实例很困难。看起来有一些讨论可以在 React 中改进这一点,但目前还没有简单的方法来做到这一点。我怀疑它尚未构建的原因可能是因为组件的编写方式是,无论组件的状态如何,卸载后的 setState 都是不可能的。就 React 团队而言,问题始终出在 Component 代码中,而不是 Component 实例中,这就是您获得 Component Type 名称的原因。

                          这个答案可能不令人满意,但我想我可以解决你的问题。

                          2) Lodashes 节流函数有一个cancel 方法。在componentWillUnmount 中调用cancel 并放弃isComponentMounted。取消比引入新属性更“惯用” React。

                          【讨论】:

                          • 问题是,我不直接控制TextLayerInternal。因此,我不知道“谁的错是setState() 电话”。我会按照您的建议尝试cancel,看看效果如何,
                          • 不幸的是,我仍然看到警告。请检查更新 1 部分中的代码,以验证我正在以正确的方式做事。
                          猜你喜欢
                          • 2021-07-01
                          • 2020-11-21
                          • 2019-11-09
                          • 2019-05-30
                          相关资源
                          最近更新 更多