问题

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

当前回答

我也遇到过类似的问题,并解决了它:

通过在redux上调度一个动作,我自动地让用户登录 (将身份验证令牌置于redux状态)

然后我试图用这个来传达一个信息。设置状态({succ_message:“……”) 在我的分量中。

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

在我看完沃尔特在这个帖子里的回答之后

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

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

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

其他回答

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

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

我有这个警告可能是因为从一个效果钩子调用setState(这将在这3个问题中讨论)。

不管怎样,升级react版本删除了警告。

在我的类似登录的屏幕的例子中,获取是在父组件的onClick处理程序中完成的,父组件将该处理程序传递给子组件,子组件将.catch和.finally放在它上面。

在.then情况下,重定向(因此卸载)将作为正常操作发生,只有在获取错误的情况下,子进程才会挂载在屏幕上。

我的解决方案是将setState和所有其他代码从.finally移到.catch,因为子对象肯定会挂载在.catch case中。在.then情况下,由于保证了卸载,所以不需要做任何事情。

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

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

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

  return <div ref={ref}>content</div>
}

尝试更改setDivSizeThrottleable为

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