So brechen Sie einen Abruf auf componentWillUnmount ab

89

Ich denke, der Titel sagt alles. Die gelbe Warnung wird jedes Mal angezeigt, wenn ich eine Komponente abhebe, die noch abgerufen wird.

Konsole

Warnung: Eine nicht gemountete Komponente kann nicht aufgerufen setState(oder forceUpdate) werden. Dies ist ein No-Op, aber ... Um dies zu beheben, kündigen Sie alle Abonnements und asynchronen Aufgaben in der componentWillUnmountMethode.

  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);
      });
  }
João Belo
quelle
Was ist es Warnung, ich habe dieses Problem nicht
Nima Moradi
Frage aktualisiert
João Belo
Haben
füge deinen
Abrufcode

Antworten:

76

Wenn Sie ein Versprechen auslösen, kann es einige Sekunden dauern, bis es aufgelöst wird. Zu diesem Zeitpunkt hat der Benutzer möglicherweise zu einem anderen Ort in Ihrer App navigiert. Wenn also Promise Resolves setStatefür eine nicht gemountete Komponente ausgeführt wird und Sie eine Fehlermeldung erhalten - genau wie in Ihrem Fall. Dies kann auch zu Speicherlecks führen.

Aus diesem Grund ist es am besten, einen Teil Ihrer asynchronen Logik aus Komponenten zu entfernen.

Andernfalls müssen Sie Ihr Versprechen irgendwie stornieren . Alternativ können Sie als letzte Möglichkeit (es handelt sich um ein Antimuster) eine Variable behalten, um zu überprüfen, ob die Komponente noch bereitgestellt ist:

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}

Ich werde das noch einmal betonen - dies ist ein Antimuster , kann aber in Ihrem Fall ausreichend sein (genau wie bei der FormikImplementierung).

Eine ähnliche Diskussion auf GitHub

BEARBEITEN:

Dies ist wahrscheinlich, wie ich das gleiche Problem (mit nichts als Reagieren) mit Hooks lösen würde :

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

OPTION B: Alternativ dazu useRefverhält es sich wie eine statische Eigenschaft einer Klasse, was bedeutet, dass keine Komponenten erneut gerendert werden, wenn sich der Wert ändert:

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
}

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

Tomasz Mularczyk
quelle
4
Es gibt also keine echte Möglichkeit, den Abruf für die Komponente WillUnmount abzubrechen.
João Belo
@ JoãoBelo Ich bin mir nicht sicher, ob es unterstützt wird. Schauen Sie sich dieses Paket an
Tomasz Mularczyk
1
Oh, ich habe den Code Ihrer Antwort vorher nicht bemerkt, es hat funktioniert. danke
João Belo
2
Was meinst du mit "Deshalb ist es am besten, deine asynchrone Logik aus Komponenten heraus zu verschieben."? Ist nicht alles in Reaktion eine Komponente?
Karpik
25

Die freundlichen Mitarbeiter von React empfehlen , Ihre Abrufe / Versprechen in ein stornierbares Versprechen zu packen. Obwohl in dieser Dokumentation keine Empfehlung enthalten ist, den Code beim Abrufen von der Klasse oder Funktion zu trennen, erscheint dies ratsam, da andere Klassen und Funktionen diese Funktionalität wahrscheinlich benötigen. Die Codeduplizierung ist ein Anti-Pattern und unabhängig vom verbleibenden Code sollte entsorgt oder storniert werden componentWillUnmount(). Gemäß React können Sie cancel()das eingewickelte Versprechen aufrufen componentWillUnmount, um zu vermeiden, dass der Status für eine nicht gemountete Komponente festgelegt wird.

Der bereitgestellte Code würde ungefähr so ​​aussehen wie diese Codefragmente, wenn wir React als Leitfaden verwenden:

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

---- BEARBEITEN ----

Ich habe festgestellt, dass die angegebene Antwort möglicherweise nicht ganz richtig ist, wenn ich dem Problem auf GitHub folge. Hier ist eine Version, die ich verwende und die für meine Zwecke funktioniert:

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

Die Idee war, dem Garbage Collector dabei zu helfen, Speicher freizugeben, indem die Funktion oder was auch immer Sie verwenden, auf null gesetzt wird.

Haleonj
quelle
Haben Sie den Link zu dem Thema auf Github
Ren
@Ren, es gibt eine GitHub- Site zum Bearbeiten der Seite und zum Besprechen von Problemen.
Haleonj
Ich bin mir nicht mehr sicher, wo genau das Problem in diesem GitHub-Projekt liegt.
Haleonj
1
Link zum GitHub-Problem: github.com/facebook/react/issues/5465
Sammalfix
20

Mit AbortController können Sie eine Abrufanforderung abbrechen.

Siehe auch: 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>

Paduado
quelle
1
Ich wünschte, ich hätte gewusst, dass es eine Web-API zum Abbrechen von Anfragen wie AbortController gibt. Aber in Ordnung, es ist nicht zu spät, es zu wissen. Danke dir.
Lex Soft
10

Seit der Eröffnung des Beitrags wurde ein "Abbruch-Abruf" hinzugefügt. https://developers.google.com/web/updates/2017/09/abortable-fetch

(aus den Dokumenten :)

Das Controller + Signal-Manöver Treffen Sie den AbortController und AbortSignal:

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

Der Controller hat nur eine Methode:

controller.abort (); Wenn Sie dies tun, wird das Signal benachrichtigt:

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

Diese API wird vom DOM-Standard bereitgestellt, und das ist die gesamte API. Es ist absichtlich generisch, sodass es von anderen Webstandards und JavaScript-Bibliotheken verwendet werden kann.

So würden Sie beispielsweise nach 5 Sekunden ein Abruf-Timeout erstellen:

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);
});
Ben Yitzhaki
quelle
Interessant, ich werde es so versuchen. Zuvor werde ich jedoch zuerst die AbortController-API lesen.
Lex Soft
Können wir nur eine AbortController-Instanz für mehrere Abrufe verwenden, sodass beim Aufrufen der Abbruchmethode dieses einzelnen AbortControllers in der Komponente WillUnmount alle vorhandenen Abrufe in unserer Komponente abgebrochen werden? Wenn nicht, müssen wir für jeden Abruf unterschiedliche AbortController-Instanzen bereitstellen, oder?
Lex Soft
3

Der Kern dieser Warnung besteht darin, dass Ihre Komponente einen Verweis darauf enthält, der von einem ausstehenden Rückruf / Versprechen gehalten wird.

Um zu vermeiden, dass Ihr isMounted-Status wie im zweiten Muster erhalten bleibt (was Ihre Komponente am Leben erhält), schlägt die Reaktionswebsite vor , ein optionales Versprechen zu verwenden . Dieser Code scheint jedoch auch Ihr Objekt am Leben zu halten.

Stattdessen habe ich einen Abschluss mit einer verschachtelten gebundenen Funktion für setState verwendet.

Hier ist mein Konstruktor (Typoskript)…

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.
    }
}
Anthony Wieser
quelle
3
Dies ist konzeptionell nicht anders als das Beibehalten einer isMounted-Flagge, nur dass Sie sie an den Verschluss binden, anstatt sie aufzuhängenthis
AnilRedshift
2

Wenn ich "alle Abonnements kündigen und asynchron" muss, sende ich normalerweise etwas an redux in componentWillUnmount, um alle anderen Abonnenten zu informieren und bei Bedarf eine weitere Anfrage zur Kündigung an den Server zu senden

Sasha Kos
quelle
2

Ich denke, wenn es nicht notwendig ist, den Server über die Stornierung zu informieren, ist es am besten, nur die asynchrone / warten-Syntax zu verwenden (falls verfügbar).

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);
  }
}
Sasha Kos
quelle
0

Zusätzlich zu den Beispielen für stornierbare Versprechen in der akzeptierten Lösung kann es nützlich sein, einen useAsyncCallbackHaken zu haben, der einen Anforderungsrückruf umschließt und ein stornierbares Versprechen zurückgibt. Die Idee ist die gleiche, aber mit einem Haken, der wie ein normaler funktioniert useCallback. Hier ist ein Beispiel für die Implementierung:

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
}
Thomas Jgenti
quelle
-2

Ich glaube, ich habe einen Weg gefunden, um das zu umgehen. Das Problem ist nicht so sehr das Abrufen selbst, sondern der setState, nachdem die Komponente verworfen wurde. Die Lösung bestand also darin, this.state.isMountedals falseund dann wieder auf componentWillMountwahr und componentWillUnmountdann wieder auf falsch zu setzen. Dann nur noch if(this.state.isMounted)den setState im Abruf. Wie so:

  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,
    })
  }
João Belo
quelle
3
setState ist wahrscheinlich nicht ideal, da der Wert im Status nicht sofort aktualisiert wird.
LeonF