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

时间:2018-12-27 18:33:42

标签: javascript reactjs typescript lodash setstate

问题

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

enter image description here

代码

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;

22 个答案:

答案 0 :(得分:79)

这是针对

React Hooks 的特定解决方案

错误

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

解决方案

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

useEffect(() => {
  let isMounted = true; // note this flag denote mount status
  someAsyncOperation().then(data => {
    if (isMounted) setState(data);
  })
  return () => { isMounted = false }; // use effect cleanup to set flag false, if unmounted
});

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; // note this mounted flag
    fetchData();
    return () => {
      isMounted = false;
    }; // use effect cleanup to set flag false, if unmounted

    // simulate some Web API fetching
    function fetchData() {
      setTimeout(() => {
        // drop "if (isMounted)" to trigger error again
        if (isMounted) setState("data fetched");
      }, 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 isMounted = true;
    asyncFn().then(data => {
      if (isMounted) onSuccess(data);
    });
    return () => { isMounted = false };
  }, [asyncFn, onSuccess]);
}

// use async operation with automatic abortion on unmount
function useAsync(asyncFn, onSuccess) {
  useEffect(() => {
    let isMounted = true;
    asyncFn().then(data => {
      if (isMounted) onSuccess(data);
    });
    return () => {
      isMounted = false;
    };
  }, [asyncFn, onSuccess]);
}

const Child = () => {
  const [state, setState] = useState("loading (4 sec)...");
  useAsync(delay, 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 delay = () => 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>

答案 1 :(得分:32)

如果上述解决方案不起作用,请尝试此操作,它对我有用:

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

答案 2 :(得分:4)

我之所以发出此警告,可能是因为从效果挂钩中调用了<span class="emoji1"><i></i></span> <span class="emoji2"><i></i></span>(这在这3 issues linked together中进行了讨论)。

无论如何,升级react版本会删除警告。

答案 3 :(得分:4)

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

答案 4 :(得分:3)

  

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

答案 5 :(得分:2)

有一个相当普遍的钩子称为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); }
    })
  });
  ...
}

答案 6 :(得分:2)

如果您正在从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;
    };
}, []);

答案 7 :(得分:1)

尝试将setDivSizeThrottleable更改为

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

答案 8 :(得分:1)

由于我有许多不同的 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);
}

答案 9 :(得分:1)

由于@ ford04帮助我,我遇到了类似的问题。

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

NB。我正在使用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 />
  </>
)

答案 10 :(得分:1)

编辑:我刚刚意识到警告正在引用名为TextLayerInternal的组件。这可能是您的错误所在。其余的这些仍然有用,但是可能无法解决您的问题。

1)为此警告获取组件实例非常困难。在React中似乎有一些讨论可以改善此问题,但目前尚无简便方法。我怀疑它尚未构建的原因可能是因为期望组件以这样一种方式编写:无论组件的状态如何,都无法在卸载后使用setState。就React团队而言,问题始终在组件代码中,而不是在组件实例中,这就是为什么要获得组件类型名称的原因。

该答案可能无法令人满意,但是我认为我可以解决您的问题。

2)Lodashes的节流函数具有cancel方法。在cancel中呼叫componentWillUnmount,并抛弃isComponentMounted。与引入新属性相比,取消更像是“习惯上”的React。

答案 11 :(得分:0)

基于@ 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> ... ;
} ;

答案 12 :(得分:0)

根据您打开网页的方式,可能不会导致安装。例如使用<Link/>返回虚拟DOM中已挂载的页面,因此会捕获来自componentDidMount生命周期的数据。

答案 13 :(得分:0)

函数式组件方法(Minimal DemoFull Demo):

import React, { useState } from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpFetch from "cp-fetch"; //cancellable c-promise fetch wrapper

export default function TestComponent(props) {
  const [text, setText] = useState("");

  useAsyncEffect(
    function* () {
      setText("fetching...");
      const response = yield cpFetch(props.url);
      const json = yield response.json();
      setText(`Success: ${JSON.stringify(json)}`);
    },
    [props.url]
  );

  return <div>{text}</div>;
}

类组件(Live demo

import React from "react";
import { ReactComponent, timeout } from "c-promise2";
import cpFetch from "cp-fetch";

@ReactComponent
class TestComponent extends React.Component {
  state = {
    text: "fetching..."
  };

  @timeout(5000)
  *componentDidMount() {
    const response = yield cpFetch(this.props.url);
    this.setState({ text: `json: ${yield response.text()}` });
  }

  render() {
    return <div>{this.state.text}</div>;
  }
}

答案 14 :(得分:0)

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

我通过在redux上调度操作来自动使用户登录 (将身份验证令牌置于redux状态)

然后我试图用this.setState({succ_message:“ ...”) 在我的组件中。

组件看上去空着,并在控制台上出现了相同的错误:“未安装的组件” ..“内存泄漏”等。

在此线程上读完Walter的答案后

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

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

无论令牌是否存在,我都使该路由可见。

答案 15 :(得分:0)

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

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

答案 16 :(得分:0)

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

代码报告了错误:

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

修复后的代码:

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

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

答案 17 :(得分:-1)

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

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

答案 18 :(得分:-1)

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

答案 19 :(得分:-1)

  React.useEffect(() => { 
    let unsubscribe;
    const getUser = async () => {
      unsubscribe = await auth.onAuthStateChanged((user) => {
        if (user) {
          const userRef = createUserProfileDocument(user);
          userRef
            .then((ref) => ref.get())
            .then((getData) => {
              setCurrentUser({ id: getData.id, ...getData.data() });
            });
        }
      });
    };
    getUser();
    return function cleanup() {
      unsubscribe();
    };
  }, [setCurrentUser]);

答案 20 :(得分:-1)

最简单、最紧凑的解决方案(附有说明)如下所示为单行解决方案。

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

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

这个非常简单的解决方案就是所需要的。此外,它还与 @ford04 和 @sfletche 提供的虚构语法不同。顺便说一句,下面来自 @ford04 的代码片段纯粹是虚构的语法(@sfletche@vinod@guneetgstar@Drew Cordano 使用了完全相同的虚构语法) .

data => { <--- 虚构/虚构的语法

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

我的所有 linter 和我整个团队的所有 linter 都不会接受它,他们会报告 Uncaught SyntaxError: unexpected token: '=>'。我很惊讶没有人发现虚构的语法。任何参与过此问题线程的人,尤其是支持投票的人,能否向我解释他们是如何找到适合他们的解决方案的?

答案 21 :(得分:-2)

受@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 正在从后端执行某种获取操作,则在卸载组件时中止它通常是有意义的(但并非总是如此,有时如果例如您正在将一些数据加载到存储中,您也可以即使组件被卸载也只想完成它)