마운트되지 않은 구성 요소에서 React 상태 업데이트를 수행 할 수 없습니다.


143

문제

나는 반작용에서 응용 프로그램을 작성하고 호출 슈퍼 일반적인 함정 피하기 할 수 없습니다입니다 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

여기에 이미지 설명 입력

암호

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;

리스너 추가 및 제거를 주석 처리해도 문제가 지속됩니까?
ic3b3rg

어떤 이벤트 청취 코드가 존재하지 않는 경우는 @ ic3b3rg 문제가 사라집니다
이고르 Soloydenko

알겠습니다 this.setDivSizeThrottleable.cancel(). this.isComponentMounted경비원 대신 제안 해 보셨나요?
ic3b3rg

1
@ ic3b3rg 여전히 동일한 런타임 경고입니다.
Igor Soloydenko

답변:


94

다음은 React Hooks 특정 솔루션입니다.

오류

경고 : 마운트 해제 된 구성 요소에서 React 상태 업데이트를 수행 할 수 없습니다.

해결책

구성 요소가 마운트 해제되는 즉시 정리 콜백에서 변경 될 let isMounted = trueinside 를 선언 할 수 있습니다 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
});

확장 : Custom useAsyncHook

모든 상용구를 사용자 지정 후크로 캡슐화 할 수 있습니다. 사용자 지정 후크는 다음과 같이 구성 요소가 마운트 해제되기 전에 비동기 함수를 처리하고 자동으로 중단하는 방법을 알고 있습니다.

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


2
당신의 트릭이 작동합니다! 뒤에 마술이 무엇인지 궁금합니다.
Niyongabo

1
여기서는 종속성이 변경되거나 구성 요소가 마운트 해제 될 때 실행되는 기본 제공 효과 정리 기능을 활용합니다 . 따라서 이것은 주변 효과 콜백 클로저 범위에서 액세스 할 수 있는 isMounted플래그를 로 전환 할 수있는 완벽한 장소 false입니다. 정리 기능 은 해당 효과에 속하는 것으로 생각할 수 있습니다 .
ford04

1
말이 되는군요! 귀하의 답변에 만족합니다. 나는 그것으로부터 배웠다.
Niyongabo

1
@VictorMolina 아니요, 확실히 과잉 일 것입니다. a) fetchin useEffect및 b)와 같은 비동기 작업을 사용하는 구성 요소에 대해이 기술을 고려하십시오 . 즉, 비동기 결과가 반환되고 상태로 설정 될 준비가되기 전에 마운트 해제 될 수 있습니다.
ford04

1
stackoverflow.com/a/63213676medium.com/better-programming/… 흥미로 웠지만 궁극적으로 귀하의 대답은 마침내 제 작업에 도움이되었습니다. 감사!
Ryan

87

제거하려면-마운트되지 않은 구성 요소 경고에 대해 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() {
    ...
  }
}

3
이것은 효과가 있었지만 왜 효과가 있습니까? 이 오류의 원인은 정확히 무엇입니까? 그리고 이것이 어떻게 고쳐 졌는지 : |
Abhinav

잘 작동합니다. setState를 호출하기 전에 _isMounted 값의 유효성을 검사 한 다음 componentWillUnmount ()에서 마지막으로 다시 false로 재설정되므로 setState 메서드의 반복적 인 호출을 중지합니다. 그것이 작동하는 방식이라고 생각합니다.
Abhishek

8
후크 구성 요소의 경우 다음을 사용하십시오.const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
x-magix

@ x-magix 이것에 대한 ref가 실제로 필요하지는 않습니다. 반환 함수가 닫을 수있는 지역 변수를 사용할 수 있습니다.
Mordechai

@Abhinav 이것이 작동하는 이유는 _isMountedReact가 관리 state하지 않기 때문에 React의 렌더링 파이프 라인 의 영향을받지 않는다는 것 입니다. 문제는 컴포넌트가 마운트 해제되도록 설정 될 때 React가 모든 호출을 대기열에서 빼낸다 setState()는 것입니다 ( 're-render'를 트리거합니다). 따라서 상태는 업데이트되지 않습니다
Lightfire228

35

위의 솔루션이 작동하지 않으면 이것을 시도하면 저에게 효과적입니다.

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

1
감사합니다. 누구든지이 코드를 설명 할 수 있습니까?
Badri Paudel 20.06.20

@BadriPaudel은 구성 요소를 이스케이프 할 때 null을 반환합니다. 더 이상 메모리에 데이터를 보관하지 않습니다
May'Habit

정말 감사합니다!
Tushar Gupta

뭘 돌려줘? 그대로 붙여 넣을까요?
추가로

11

이 경고 setState는 효과 후크에서 호출했기 때문일 수 있습니다 (이는 함께 연결된 이 세 가지 문제 에서 논의 됨 ).

어쨌든 반응 버전을 업그레이드하면 경고가 제거되었습니다.


5

변경 시도 setDivSizeThrottleable

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

나는 그것을 시도했다. 이제이 변경을 수행하기 전에 창 크기를 조정할 때 가끔씩 만 관찰하는 경고가 지속적으로 표시됩니다. ¯_ (ツ) _ / ¯ 시도해 주셔서 감사합니다.
이고르 Soloydenko

5

히스토리를 사용하지 않는다는 것을 알고 있지만 제 경우에는 useHistoryReact Context Provider에서 상태가 유지되기 전에 구성 요소를 마운트 해제하는 React Router DOM 의 후크를 사용하고있었습니다 .

이 문제를 해결하기 위해 withRouter필자의 경우에는 구성 요소를 중첩하고 구성 요소 export default withRouter(Login)내부 에 후크를 사용했습니다 const Login = props => { ...; props.history.push("/dashboard"); .... 나는 또한 props.history.push구성 요소 에서 다른 하나 를 제거했습니다 . 예를 들어 if(authorization.token) return props.history.push('/dashboard')이것은 authorization상태 때문에 루프를 유발하기 때문 입니다.

새 항목을 기록 에 푸시하는 대안 .


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

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

1

편집 : 경고가라는 구성 요소를 참조하고 있음을 깨달았습니다 TextLayerInternal. 버그가있을 가능성이 높습니다. 나머지는 여전히 관련이 있지만 문제를 해결하지 못할 수도 있습니다.

1)이 경고에 대한 구성 요소의 인스턴스를 얻는 것은 어렵습니다. React에서이를 개선하기위한 논의가있는 것처럼 보이지만 현재로서는 쉽게 할 수있는 방법이 없습니다. 아직 빌드되지 않은 이유는 구성 요소의 상태에 관계없이 마운트 해제 후 setState가 불가능한 방식으로 구성 요소가 작성 될 것으로 예상되기 때문일 것입니다. React 팀에 관한 한 문제는 항상 Component 인스턴스가 아닌 Component 코드에 있으므로 Component Type 이름을 얻습니다.

답변이 만족스럽지 않을 수 있지만 문제를 해결할 수 있다고 생각합니다.

2) Lodashes 제한 기능에는 cancel방법이 있습니다. 전화 cancelcomponentWillUnmount걸어 isComponentMounted. 취소는 새 속성을 도입하는 것보다 "관상 적으로"반응합니다.


문제는 내가 직접 제어하지 않는다는 것 TextLayerInternal입니다. 따라서 나는 "누가 잘못이 setState()부름인가"를 모른다 . 나는 해보자 cancel귀하의 조언에 따라하고, 어떻게되는지
이고르 Soloydenko

불행히도 여전히 경고가 표시됩니다. 업데이트 1 섹션의 코드를 확인하여 올바른 방식으로 작업하고 있는지 확인하십시오.
Igor Soloydenko

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 />
  </>
)

1

웹 페이지를 여는 방법에 따라 마운트가 발생하지 않을 수 있습니다. 예를 들어 <Link/>, 가상 DOM에 이미 마운트 된 페이지 로 돌아가는 것을 사용하여 componentDidMount 라이프 사이클의 데이터를 요구합니다.


당신은 건가요 componentDidMount()중간없이 두 번 호출 할 수 componentWillUnmount()사이에 전화? 나는 그것이 가능하다고 생각하지 않습니다.
Alexis Wilke

1
아니,이 페이지가 코드 내부를 처리하지 않는 이유입니다 두 번 호출되지 않는다는 것을 말하고 componentDidMount()를 사용하는 경우 <Link/>. 이러한 문제에 Redux를 사용하고 웹 페이지의 데이터를 Reducer 저장소에 보관하므로 페이지를 다시로드 할 필요가 없습니다.
coder9833idls

0

비슷한 문제가 있었고 해결했습니다.

redux에 대한 작업을 발송하여 사용자가 자동으로 로그인하도록했습니다 (redux 상태에 인증 토큰 배치).

그런 다음 내 구성 요소에서 this.setState ({succ_message : "...") 메시지를 표시하려고했습니다.

콘솔에 "unmounted component".. "memory leak"등의 오류와 함께 구성 요소가 비어있는 것처럼 보입니다.

이 스레드에서 Walter의 답변을 읽은 후

내 응용 프로그램의 라우팅 테이블에서 사용자가 로그인 한 경우 구성 요소의 경로가 유효하지 않음을 확인했습니다.

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

토큰의 존재 여부에 관계없이 경로를 표시했습니다.


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> ... ;
} ;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.