Không thể thực hiện cập nhật trạng thái React trên một thành phần chưa được gắn kết


125

Vấn đề

Tôi đang viết một ứng dụng trong React và không thể tránh khỏi một lỗi siêu phổ biến, đang kêu gọi setState(...)sau componentWillUnmount(...).

Tôi đã xem xét rất kỹ mã của mình và cố gắng đặt một số điều khoản bảo vệ tại chỗ, nhưng sự cố vẫn tiếp diễn và tôi vẫn đang quan sát cảnh báo.

Do đó, tôi có hai câu hỏi:

  1. Làm cách nào để tìm ra từ dấu vết ngăn xếp , thành phần cụ thể và trình xử lý sự kiện hoặc móc vòng đời nào chịu trách nhiệm cho vi phạm quy tắc?
  2. Chà, làm thế nào để tự khắc phục sự cố, bởi vì mã của tôi được viết với sự cố này và đang cố gắng ngăn chặn nó, nhưng một số thành phần cơ bản vẫn tạo ra cảnh báo.

Bảng điều khiển trình duyệt

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

nhập mô tả hình ảnh ở đây

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

Cập nhật 1: Hủy chức năng điều tiết (vẫn không có may mắn)

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;

Sự cố vẫn tiếp diễn nếu bạn nhận xét thêm và xóa người nghe?
ic3b3rg 27/12/18

@ ic3b3rg, sự cố sẽ biến mất nếu không có mã nghe sự kiện
Igor Soloydenko 27/12/18

ok, bạn đã thử đề xuất làm this.setDivSizeThrottleable.cancel()thay vì this.isComponentMountedbảo vệ chưa?
ic3b3rg

1
@ ic3b3rg Vẫn cảnh báo thời gian chạy như cũ.
Igor Soloydenko

Câu trả lời:


68

Đây là một giải pháp cụ thể cho React Hooks cho

lỗi

Cảnh báo: Không thể thực hiện cập nhật trạng thái React trên một thành phần chưa được gắn kết.

Giải pháp

Bạn có thể khai báo let isMounted = truebên trong useEffect, điều này sẽ được thay đổi trong lệnh gọi lại dọn dẹp, ngay sau khi thành phần được ngắt kết nối. Trước khi cập nhật trạng thái, bây giờ bạn kiểm tra biến này có điều kiện:

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

Phần mở rộng: useAsyncMóc tùy chỉnh

Chúng ta có thể đóng gói tất cả các bản ghi sẵn vào một Hook tùy chỉnh, chỉ cần biết, cách xử lý và tự động hủy bỏ các hàm không đồng bộ trong trường hợp thành phần ngắt kết nối trước đó:

function useAsync(asyncFn, onSuccess) {
  useEffect(() => {
    let isMounted = true;
    asyncFn().then(data => {
      if (isMounted) onSuccess(data);
    });
    return () => { isMounted = false };
  }, [asyncFn, onSuccess]);
}


1
thủ thuật của bạn hoạt động! Tôi tự hỏi điều kỳ diệu đằng sau là gì?
Niyongabo

1
Chúng tôi tận dụng tính năng dọn dẹp hiệu ứng tích hợp ở đây, tính năng này chạy khi các phần phụ thuộc thay đổi và trong cả hai trường hợp khi thành phần ngắt kết nối. Vì vậy, đây là nơi hoàn hảo để chuyển đổi một isMountedcờ false, có thể được truy cập từ phạm vi đóng lệnh gọi lại hiệu ứng xung quanh. Bạn có thể coi chức năng dọn dẹp thuộc về hiệu ứng tương ứng của nó.
ford04

1
nó có ý nghĩa! Tôi hài lòng với câu trả lời của bạn. Tôi đã học được từ nó.
Niyongabo

hút thuốc thánh ... isMountedthứ này hoạt động. Tôi đang thử nghiệm phản ứng-lib 10.4.7và formik ^2.1.4. Điều này giống như một vụ hack tổng thể và là kết quả của một cái gì đó với Formik.
Phil Lucks

1
@VictorMolina Không, điều đó chắc chắn sẽ là quá mức cần thiết. Hãy xem xét kỹ thuật này cho các thành phần a) sử dụng các hoạt động không đồng bộ như fetchtrong useEffectvà b) không ổn định, tức là có thể được ngắt kết nối trước khi kết quả không đồng bộ trả về và sẵn sàng được đặt làm trạng thái.
ford04

83

Để loại bỏ - Không thể thực hiện cập nhật trạng thái React đối với cảnh báo thành phần chưa được gắn kết, hãy sử dụng phương thức componentDidMount trong một điều kiện và đặt sai điều kiện đó trên phương thức componentWillUnmount. Ví dụ : -

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() {
    ...
  }
}

2
Điều này đã hiệu quả, nhưng tại sao nó phải hoạt động? Chính xác thì điều gì gây ra lỗi này? và điều này đã khắc phục nó như thế nào: |
Abhinav

Nó hoạt động tốt. Nó dừng lệnh gọi lặp đi lặp lại của phương thức setState vì nó xác thực giá trị _isMounted trước cuộc gọi setState, sau đó cuối cùng được đặt lại thành false trong componentWillUnmount (). Tôi nghĩ, đó là cách nó hoạt động.
Abhishek

7
đối với thành phần hook, hãy sử dụng cái này:const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
x-magix

@ x-magix Bạn không thực sự cần ref cho việc này, bạn chỉ có thể sử dụng một biến cục bộ mà hàm trả về có thể đóng.
Mordechai

@Abhinav Dự đoán tốt nhất của tôi tại sao điều này hoạt động là điều _isMountednày không được quản lý bởi React (không giống như state) và do đó không phải tuân theo quy trình kết xuất của React . Vấn đề là khi một thành phần được thiết lập để không được gắn kết, React sẽ xếp hàng bất kỳ lệnh gọi nào đến setState()(điều này sẽ kích hoạt 're-render'); do đó, trạng thái không bao giờ được cập nhật
Lightfire228

26

Nếu các giải pháp trên không hoạt động, hãy thử cách này và nó phù hợp với tôi:

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

Cảm ơn nó hoạt động cho tôi. Bất cứ ai có thể giải thích cho tôi đoạn mã này?
Badri Paudel

@BadriPaudel trả về null khi thành phần thoát, nó sẽ không còn giữ bất kỳ dữ liệu nào trong bộ nhớ
Ngày

Cám ơn bạn rất nhiều về điều này.
Tushar Gupta

trả lại cái gì? chỉ cần dán nó như nó là?
cộng với


5

thử thay đổi setDivSizeThrottleablethành

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

Tôi đã thử nó. Bây giờ tôi thường xuyên thấy cảnh báo mà tôi chỉ quan sát thỉnh thoảng về việc thay đổi kích thước cửa sổ trước khi thực hiện thay đổi này. ¯_ (ツ) _ / ¯ Cảm ơn bạn đã thử cái này.
Igor Soloydenko

3

Tôi biết rằng bạn không sử dụng lịch sử, nhưng trong trường hợp của tôi, tôi đang sử dụng useHistoryhook từ React Router DOM, ngắt kết nối thành phần trước khi trạng thái được duy trì trong React Context Provider.

Để khắc phục sự cố này, tôi đã sử dụng hook withRouterlồng thành phần, trong trường hợp của tôi export default withRouter(Login)và bên trong thành phần const Login = props => { ...; props.history.push("/dashboard"); .... Tôi cũng đã loại bỏ cái khác props.history.pushkhỏi thành phần, ví dụ: if(authorization.token) return props.history.push('/dashboard')vì điều này gây ra vòng lặp, vì authorizationtrạng thái.

Một giải pháp thay thế để đẩy một mục mới vào lịch sử .


2

Nếu bạn đang tìm nạp dữ liệu từ axios và lỗi vẫn xảy ra, chỉ cần bọc bộ định mức bên trong điều kiện

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

1

Chỉnh sửa: Tôi vừa nhận ra cảnh báo đang tham chiếu đến một thành phần được gọi TextLayerInternal. Đó có thể là lỗi của bạn. Phần còn lại của điều này vẫn có liên quan, nhưng nó có thể không khắc phục được sự cố của bạn.

1) Lấy ví dụ của một thành phần cho cảnh báo này là khó. Có vẻ như có một số cuộc thảo luận để cải thiện điều này trong React nhưng hiện tại không có cách nào dễ dàng để làm điều đó. Tôi nghi ngờ lý do nó chưa được xây dựng có thể là do các thành phần được mong đợi được viết theo cách mà setState sau khi ngắt kết nối là không thể thực hiện được bất kể trạng thái của thành phần đó là gì. Theo như nhóm React có liên quan, vấn đề luôn nằm trong mã Thành phần chứ không phải cá thể Thành phần, đó là lý do tại sao bạn nhận được tên Loại Thành phần.

Câu trả lời đó có thể không thỏa đáng, nhưng tôi nghĩ tôi có thể khắc phục sự cố của bạn.

2) Chức năng điều tiết đèn flash có một cancelphương pháp. Gọi cancelvào componentWillUnmountvà bỏ qua isComponentMounted. Việc hủy bỏ là một phản ứng "thành ngữ" hơn là giới thiệu một thuộc tính mới.


Vấn đề là, tôi không trực tiếp kiểm soát TextLayerInternal. Vì vậy, tôi không biết " setState()cuộc gọi là lỗi của ai ". Tôi sẽ cố gắng canceltheo lời khuyên của bạn và xem làm thế nào nó đi,
Igor Soloydenko

Thật không may, tôi vẫn thấy cảnh báo. Vui lòng kiểm tra mã trong phần Cập nhật 1 để xác minh rằng tôi đang thực hiện đúng cách.
Igor Soloydenko

1

Tôi đã gặp vấn đề tương tự, cảm ơn @ ford04 đã giúp tôi.

Tuy nhiên, một lỗi khác đã xảy ra.

NB. Tôi đang sử dụng móc 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.

Nguyên nhân gây ra lỗi?

import {useHistory} from 'react-router-dom'

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

Điều này không thể hoạt động vì mặc dù bạn đang chuyển hướng đến trang mới, tất cả trạng thái và đạo cụ đang được thao tác trên dom hoặc đơn giản là hiển thị về trang trước đó vẫn chưa dừng lại.

Tôi đã tìm ra giải pháp nào

import {Redirect} from 'react-router-dom'

if (true) {
  return <redirect to="/new-route" />
}
return (
  <>
    <render component />
  </>
)

0

Tôi đã gặp sự cố tương tự và đã giải quyết nó:

Tôi đã tự động làm cho người dùng đăng nhập bằng cách thực hiện một hành động trên redux (đặt mã thông báo xác thực trên trạng thái redux)

và sau đó tôi đang cố gắng hiển thị thông báo với this.setState ({succ_message: "...") trong thành phần của mình.

Thành phần trông trống rỗng với cùng một lỗi trên bảng điều khiển: "thành phần chưa được gắn kết" .. "rò rỉ bộ nhớ", v.v.

Sau khi tôi đọc câu trả lời của Walter trong chủ đề này

Tôi nhận thấy rằng trong bảng Định tuyến của ứng dụng của mình, tuyến của thành phần của tôi không hợp lệ nếu người dùng đã đăng nhập:

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

Tôi đã hiển thị Tuyến đường cho dù mã thông báo có tồn tại hay không.


0

Dựa trên câu trả lời @ ford04, đây là câu trả lời tương tự được đóng gói trong một phương thức:

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

Sử dụng:

const MyComponent : FC<{}> = (props) => {
    const [ asyncProp , setAsyncProp ] = useState( '' ) ;
    useEffectAsync( async ( isMounted ) =>
    {
        const someAsyncProp = await ... ;
        if ( isMounted() )
             setAsyncProp( someAsyncProp ) ;
    });
    return <div> ... ;
} ;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.