Jak anulować pobieranie na componentWillUnmount


90

Myślę, że tytuł mówi wszystko. Żółte ostrzeżenie jest wyświetlane za każdym razem, gdy odmontowuję komponent, który wciąż się pobiera.

Konsola

Ostrzeżenie: nie można wywołać setState(lub forceUpdate) na niezmontowanym komponencie. To nie jest operacja, ale ... Aby naprawić, anuluj wszystkie subskrypcje i zadania asynchroniczne w componentWillUnmountmetodzie.

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

co to za ostrzeżenie, nie mam tego problemu
nima moradi

pytanie zaktualizowane
João Belo


dodaj swój kod do
qustion

Odpowiedzi:


80

Kiedy uruchomisz obietnicę, może minąć kilka sekund, zanim zostanie rozwiązana, a do tego czasu użytkownik mógł przejść do innego miejsca w Twojej aplikacji. Więc kiedy Promise rozwiązuje problem, setStatejest wykonywany na niezmontowanym komponencie i pojawia się błąd - tak jak w twoim przypadku. Może to również powodować wycieki pamięci.

Dlatego najlepiej jest przenieść część logiki asynchronicznej z komponentów.

W przeciwnym razie będziesz musiał w jakiś sposób anulować swoją obietnicę . Alternatywnie - w ostateczności (jest to antywzór) - możesz zachować zmienną, aby sprawdzić, czy komponent jest nadal zamontowany:

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}

Podkreślę to jeszcze raz - jest to przeciwieństwo, ale w twoim przypadku może wystarczyć (podobnie jak w przypadku Formikimplementacji).

Podobna dyskusja na GitHubie

EDYTOWAĆ:

Prawdopodobnie w ten sposób rozwiązałbym ten sam problem (nie mając nic poza Reagowaniem) z hookami :

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

OPCJA B: Alternatywnie, z useRefktórym zachowuje się jak statyczna właściwość klasy, co oznacza, że ​​nie zwraca komponentu, gdy zmienia się jego wartość:

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
}

Przykład: https://codesandbox.io/s/86n1wq2z8


4
więc nie ma prawdziwego sposobu, aby po prostu anulować pobieranie w moduleWillUnmount?
João Belo

1
Och, wcześniej nie zauważyłem kodu Twojej odpowiedzi, zadziałało. dzięki
João Belo


2
co masz na myśli, mówiąc „Dlatego najlepiej jest przenieść logikę asynchroniczną z komponentów”? Czy wszystko w reakcji nie jest komponentem?
Karpik,

1
@Tomasz Mularczyk Dziękuję bardzo, zrobiłeś coś godnego.
KARTHIKEYAN.A

25

Przyjaźni ludzie w React zalecają zawijanie połączeń / obietnic dotyczących pobierania w obietnicę, którą można anulować. Chociaż w tej dokumentacji nie ma zalecenia, aby trzymać kod oddzielnie od klasy lub funkcji podczas pobierania, wydaje się to wskazane, ponieważ inne klasy i funkcje prawdopodobnie będą potrzebować tej funkcjonalności, duplikacja kodu jest anty-wzorcem i niezależnie od tego, który kod pozostaje należy zutylizować lub anulować w componentWillUnmount(). Zgodnie z Reactem możesz odwołać cancel()się do opakowanej obietnicy, componentWillUnmountaby uniknąć ustawiania stanu niezmontowanego komponentu.

Dostarczony kod wyglądałby mniej więcej tak, jak te fragmenty kodu, jeśli użyjemy Reacta jako przewodnika:

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

---- EDYTOWAĆ ----

Stwierdziłem, że podana odpowiedź może nie być do końca poprawna, śledząc problem na GitHub. Oto jedna wersja, której używam, która działa do moich celów:

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

Pomysł polegał na tym, aby pomóc zbieraczowi elementów bezużytecznych zwolnić pamięć, ustawiając funkcję lub cokolwiek innego na wartość null.


czy masz link do problemu na github
Ren

@Ren, istnieje witryna GitHub do edytowania strony i omawiania problemów.
haleonj

Nie jestem już pewien, gdzie dokładnie występuje problem w tym projekcie GitHub.
haleonj


22

Możesz użyć AbortController, aby anulować żądanie pobierania.

Zobacz też: 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
Żałuję, że nie wiedziałem, że istnieje internetowy interfejs API do anulowania żądań, takich jak AbortController. Ale w porządku, nie jest za późno, aby to wiedzieć. Dziękuję Ci.
Lex Soft

11

Od czasu otwarcia posta dodano opcję „abortable-fetch”. https://developers.google.com/web/updates/2017/09/abortable-fetch

(z dokumentacji :)

Kontroler + manewr sygnału Poznaj AbortController i AbortSignal:

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

Kontroler ma tylko jedną metodę:

controller.abort (); Kiedy to zrobisz, powiadamia sygnał:

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

To API jest dostarczane przez standard DOM i to jest całe API. Celowo jest ogólna, więc może być używana przez inne standardy sieciowe i biblioteki JavaScript.

na przykład oto jak ustawić limit czasu pobierania po 5 sekundach:

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

Ciekawe, spróbuję w ten sposób. Ale wcześniej najpierw przeczytam API AbortController.
Lex Soft

Czy możemy użyć tylko jednej instancji AbortController do wielu pobrań, tak że po wywołaniu metody abort tego pojedynczego AbortController w komponencie componentWillUnmount anuluje ona wszystkie istniejące operacje pobierania w naszym komponencie? Jeśli nie, oznacza to, że musimy zapewnić różne instancje AbortController dla każdego pobierania, prawda?
Lex Soft

3

Istotą tego ostrzeżenia jest to, że twój komponent ma do niego odniesienie, które jest przechowywane przez jakieś zaległe wywołanie zwrotne / obietnicę.

Aby uniknąć anty-wzorców utrzymywania stanu isMounted wokół (który utrzymuje komponent przy życiu), jak to zostało zrobione w drugim wzorcu, witryna reaktora sugeruje użycie opcjonalnej obietnicy ; jednak ten kod wydaje się również utrzymywać obiekt przy życiu.

Zamiast tego zrobiłem to, używając zamknięcia z zagnieżdżoną funkcją powiązaną do setState.

Oto mój konstruktor (maszynopis)…

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
Koncepcyjnie nie różni się to od trzymania flagi isMounted, tylko że wiążesz ją z zamknięciem zamiast wieszaćthis
AnilRedshift

2

Kiedy muszę „anulować wszystkie subskrypcje i asynchroniczne”, zwykle wysyłam coś, aby zmniejszyć komponentWillUnmount, aby poinformować wszystkich innych subskrybentów i wysłać jeszcze jedno żądanie anulowania do serwera, jeśli to konieczne


2

Myślę, że jeśli nie jest konieczne informowanie serwera o anulowaniu - najlepszym podejściem jest użycie składni async / await (jeśli jest dostępna).

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

Oprócz przykładów z możliwością anulowania haków obietnicy w przyjętym rozwiązaniu przydatne może być zastosowanie useAsyncCallbackpodpięcia zawijającego żądanie wywołania zwrotnego i zwracającego anulowaną obietnicę. Pomysł jest ten sam, ale z haczykiem działającym jak zwykły useCallback. Oto przykład realizacji:

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

Myślę, że wymyśliłem sposób na obejście tego. Problemem nie jest samo pobieranie, ale setState po odrzuceniu komponentu. Tak więc rozwiązaniem było ustawienie this.state.isMountedjako, falsea następnie componentWillMountzmiana na true i componentWillUnmountponownie ustawienie na fałsz. Następnie po prostu if(this.state.isMounted)setState wewnątrz pobierania. Tak jak to:

  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 prawdopodobnie nie jest idealny, ponieważ nie zaktualizuje natychmiast wartości w stanie.
LeonF
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.