componentWillUnmount에서 가져 오기를 취소하는 방법


90

제목이 다 나와 있다고 생각합니다. 여전히 페치중인 구성 요소를 마운트 해제 할 때마다 노란색 경고가 표시됩니다.

콘솔

경고 : 마운트 해제 된 구성 요소 에서는 호출 setState(또는 forceUpdate) 할 수 없습니다 . 이것은 작동하지 않지만 ... 수정하려면 componentWillUnmount메서드의 모든 구독 및 비동기 작업을 취소하십시오 .

  constructor(props){
    super(props);
    this.state = {
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        this.setState({
          isLoading: false,
          dataSource: responseJson,
        }, function(){
        });
      })
      .catch((error) =>{
        console.error(error);
      });
  }

어떻게 내가 그 문제가없는 경고한다
니마 모 라디

업데이트 된 질문 중
주앙 벨로

당신은 가져 오기에 대한 약속 또는 비동기 코드 않았다
니마 모 라디

당신은 qustion에 코드를 가져 추가 할
니마 모 라디

답변:


80

Promise를 실행하면 해결되기까지 몇 초가 걸릴 수 있으며 그 때까지 사용자가 앱의 다른 위치로 이동했을 수 있습니다. 따라서 Promise resolves setState가 마운트되지 않은 구성 요소에서 실행 되면 귀하의 경우와 마찬가지로 오류가 발생합니다. 이로 인해 메모리 누수가 발생할 수도 있습니다.

그렇기 때문에 일부 비동기 논리를 구성 요소에서 이동하는 것이 가장 좋습니다.

그렇지 않으면 어떻게 든 약속을 취소해야합니다 . 또는 최후의 수단 (반 패턴)으로 구성 요소가 여전히 마운트되어 있는지 확인하기 위해 변수를 유지할 수 있습니다.

componentDidMount(){
  this.mounted = true;

  this.props.fetchData().then((response) => {
    if(this.mounted) {
      this.setState({ data: response })
    }
  })
}

componentWillUnmount(){
  this.mounted = false;
}

다시 한 번 강조하겠습니다. 이것은 반 패턴 이지만 귀하의 경우에는 충분할 수 있습니다 ( Formik구현 과 마찬가지로 ).

GitHub 에 대한 유사한 토론

편집하다:

이것은 내가와 같은 문제 (필요 아무것도하지만 반작용) 해결하지 얼마나 아마 후크 :

옵션 A :

import React, { useState, useEffect } from "react";

export default function Page() {
  const value = usePromise("https://something.com/api/");
  return (
    <p>{value ? value : "fetching data..."}</p>
  );
}

function usePromise(url) {
  const [value, setState] = useState(null);

  useEffect(() => {
    let isMounted = true; // track whether component is mounted

    request.get(url)
      .then(result => {
        if (isMounted) {
          setState(result);
        }
      });

    return () => {
      // clean up
      isMounted = false;
    };
  }, []); // only on "didMount"

  return value;
}

옵션 B : 또는 useRef클래스의 정적 속성처럼 작동하여 값이 변경 될 때 구성 요소를 다시 렌더링하지 않습니다.

function usePromise2(url) {
  const isMounted = React.useRef(true)
  const [value, setState] = useState(null);


  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  useEffect(() => {
    request.get(url)
      .then(result => {
        if (isMounted.current) {
          setState(result);
        }
      });
  }, []);

  return value;
}

// or extract it to custom hook:
function useIsMounted() {
  const isMounted = React.useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}

예 : https://codesandbox.io/s/86n1wq2z8


4
그래서 componentWillUnmount에 대한 가져 오기를 취소하는 실제 방법이 없습니다.
João Belo

1
오, 당신의 대답 코드를 전에는 몰랐습니다. 감사합니다
주앙 벨로

9
mb21

2
"이것이 구성 요소에서 비동기 논리를 이동하는 것이 가장 좋은 이유입니다."란 무엇을 의미합니까? 반응의 모든 것이 구성 요소가 아닙니까?
Karpik

1
@Tomasz Mularczyk 정말 감사합니다, 당신은 가치있는 일을했습니다.
KARTHIKEYAN.A

25

React의 친절한 사람들은 취소 가능한 약속으로 가져 오기 호출 / 약속을 포장 할 것을 권장 합니다. 해당 문서에는 가져 오기를 사용하여 코드를 클래스 또는 함수와 별도로 유지하라는 권장 사항이 없지만 다른 클래스 및 함수에이 기능이 필요할 가능성이 높고 코드 중복은 안티 패턴이며 느린 코드와 관계없이 코드를 유지하는 것이 좋습니다. 에서 폐기하거나 취소해야합니다 componentWillUnmount(). React 에 따라 마운트되지 않은 구성 요소에서 상태를 설정하지 않도록 cancel()래핑 된 promise를 호출 할 수 있습니다 componentWillUnmount.

제공된 코드는 React를 가이드로 사용하는 경우 다음 코드 스 니펫과 유사합니다.

const makeCancelable = (promise) => {
    let hasCanceled_ = false;

    const wrappedPromise = new Promise((resolve, reject) => {
        promise.then(
            val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
            error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel() {
            hasCanceled_ = true;
        },
    };
};

const cancelablePromise = makeCancelable(fetch('LINK HERE'));

constructor(props){
    super(props);
    this.state = {
        isLoading: true,
        dataSource: [{
            name: 'loading...',
            id: 'loading',
        }]
    }
}

componentDidMount(){
    cancelablePromise.
        .then((response) => response.json())
        .then((responseJson) => {
            this.setState({
                isLoading: false,
                dataSource: responseJson,
            }, () => {

            });
        })
        .catch((error) =>{
            console.error(error);
        });
}

componentWillUnmount() {
    cancelablePromise.cancel();
}

---- 편집하다 ----

GitHub의 문제에 따라 주어진 답변이 정확하지 않을 수 있음을 발견했습니다. 다음은 내 목적에 맞는 하나의 버전입니다.

export const makeCancelableFunction = (fn) => {
    let hasCanceled = false;

    return {
        promise: (val) => new Promise((resolve, reject) => {
            if (hasCanceled) {
                fn = null;
            } else {
                fn(val);
                resolve(val);
            }
        }),
        cancel() {
            hasCanceled = true;
        }
    };
};

아이디어는 가비지 수집기가 함수를 만들거나 null을 사용하여 메모리를 확보하도록 돕는 것입니다.


github의 문제에 대한 링크가 있습니까
Ren

@Ren, 페이지를 편집하고 문제를 논의 할 수 있는 GitHub 사이트 가 있습니다.
haleonj

해당 GitHub 프로젝트에서 정확한 문제가 어디에 있는지 더 이상 확신 할 수 없습니다.
haleonj


22

AbortController 를 사용 하여 가져 오기 요청을 취소 할 수 있습니다 .

또한보십시오: https://www.npmjs.com/package/abortcontroller-polyfill

class FetchComponent extends React.Component{
  state = { todos: [] };
  
  controller = new AbortController();
  
  componentDidMount(){
    fetch('https://jsonplaceholder.typicode.com/todos',{
      signal: this.controller.signal
    })
    .then(res => res.json())
    .then(todos => this.setState({ todos }))
    .catch(e => alert(e.message));
  }
  
  componentWillUnmount(){
    this.controller.abort();
  }
  
  render(){
    return null;
  }
}

class App extends React.Component{
  state = { fetch: true };
  
  componentDidMount(){
    this.setState({ fetch: false });
  }
  
  render(){
    return this.state.fetch && <FetchComponent/>
  }
}

ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>


2
AbortController와 같은 요청을 취소하는 웹 API가 있다는 사실을 알고 있었으면합니다. 그러나 좋아, 그것을 알기에 너무 늦지 않았습니다. 감사합니다.
Lex Soft

11

게시물이 열렸 기 때문에 "abortable-fetch"가 추가되었습니다. https://developers.google.com/web/updates/2017/09/abortable-fetch

(문서에서 :)

컨트롤러 + 신호 기동 AbortController와 AbortSignal을 만나보세요 :

const controller = new AbortController();
const signal = controller.signal;

컨트롤러에는 한 가지 방법 만 있습니다.

controller.abort (); 이렇게하면 다음과 같은 신호를 알립니다.

signal.addEventListener('abort', () => {
  // Logs true:
  console.log(signal.aborted);
});

이 API는 DOM 표준에 의해 제공되며 전체 API입니다. 의도적으로 일반적이므로 다른 웹 표준 및 JavaScript 라이브러리에서 사용할 수 있습니다.

예를 들어, 다음은 5 초 후에 가져 오기 시간 초과를 만드는 방법입니다.

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.log(text);
});

흥미롭게도 이렇게 해보겠습니다. 하지만 그 전에 AbortController API를 먼저 읽어 보겠습니다.
Lex Soft

componentWillUnmount에서이 단일 AbortController의 abort 메소드를 호출 할 때 컴포넌트에있는 기존의 모든 가져 오기를 취소하도록 여러 가져 오기에 하나의 AbortController 인스턴스 만 사용할 수 있습니까? 그렇지 않다면 각 가져 오기에 대해 다른 AbortController 인스턴스를 제공해야 함을 의미합니다. 맞죠?
Lex Soft

3

이 경고의 핵심은 구성 요소에 일부 미해결 콜백 / 약속이 보유한 참조가 있다는 것입니다.

두 번째 패턴에서와 같이 isMounted 상태를 유지하는 (컴포넌트를 활성 상태로 유지하는) 반 패턴을 피하기 위해 react 웹 사이트는 선택적 promise를 사용할 것을 제안 합니다 . 그러나 그 코드는 또한 객체를 살아있게 유지하는 것처럼 보입니다.

대신 setState에 중첩 된 바인딩 함수가있는 클로저를 사용하여 수행했습니다.

여기 내 생성자 (typescript)가 있습니다…

constructor(props: any, context?: any) {
    super(props, context);

    let cancellable = {
        // it's important that this is one level down, so we can drop the
        // reference to the entire object by setting it to undefined.
        setState: this.setState.bind(this)
    };

    this.componentDidMount = async () => {
        let result = await fetch(…);            
        // ideally we'd like optional chaining
        // cancellable.setState?.({ url: result || '' });
        cancellable.setState && cancellable.setState({ url: result || '' });
    }

    this.componentWillUnmount = () => {
        cancellable.setState = undefined; // drop all references.
    }
}

3
이것은 개념적으로 isMounted 플래그를 유지하는 것과 다르지 않습니다. 단지 당신이 그것을 매달리는 대신 클로저에 바인딩하는 것입니다this
AnilRedshift

2

"모든 구독을 취소하고 비 동기화"해야 할 때 일반적으로 componentWillUnmount의 redux에 무언가를 발송하여 다른 모든 구독자에게 알리고 필요한 경우 취소에 대한 요청을 하나 더 서버에 보냅니다.


2

취소에 대해 서버에 알릴 필요가 없다면 가장 좋은 방법은 async / await 구문을 사용하는 것입니다 (사용 가능한 경우).

constructor(props){
  super(props);
  this.state = {
    isLoading: true,
    dataSource: [{
      name: 'loading...',
      id: 'loading',
    }]
  }
}

async componentDidMount() {
  try {
    const responseJson = await fetch('LINK HERE')
      .then((response) => response.json());

    this.setState({
      isLoading: false,
      dataSource: responseJson,
    }
  } catch {
    console.error(error);
  }
}

0

허용 된 솔루션의 취소 가능한 약속 후크 예제 외에도 useAsyncCallback요청 콜백을 래핑하고 취소 가능한 약속을 반환하는 후크 를 사용하는 것이 편리 할 수 ​​있습니다 . 아이디어는 동일하지만 일반 useCallback. 다음은 구현의 예입니다.

function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[]) {
  const isMounted = useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false
    }
  }, [])

  const cb = useCallback(callback, dependencies)

  const cancellableCallback = useCallback(
    (...args: any[]) =>
      new Promise<T>((resolve, reject) => {
        cb(...args).then(
          value => (isMounted.current ? resolve(value) : reject({ isCanceled: true })),
          error => (isMounted.current ? reject(error) : reject({ isCanceled: true }))
        )
      }),
    [cb]
  )

  return cancellableCallback
}

-2

나는 그것에 대한 방법을 알아 낸 것 같다. 문제는 가져 오기 자체가 아니라 구성 요소가 해제 된 후의 setState입니다. 따라서 해결책은 this.state.isMountedas 로 설정 false한 다음 componentWillMounttrue로 변경하고 componentWillUnmount다시 false 로 설정하는 것입니다. 그런 다음 if(this.state.isMounted)가져 오기 내부의 setState 만 있습니다. 이렇게 :

  constructor(props){
    super(props);
    this.state = {
      isMounted: false,
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    this.setState({
      isMounted: true,
    })

    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        if(this.state.isMounted){
          this.setState({
            isLoading: false,
            dataSource: responseJson,
          }, function(){
          });
        }
      })
      .catch((error) =>{
        console.error(error);
      });
  }

  componentWillUnmount() {
    this.setState({
      isMounted: false,
    })
  }

3
setState는 상태의 값을 즉시 업데이트하지 않기 때문에 이상적이지 않습니다.
LeonF
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.