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

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

  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);
      });
  }
107
João Belo 18 Apr. 2018 im 21:15

9 Antworten

Beste Antwort

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 Promise aufgelöst wird, wird setState für eine nicht gemountete Komponente ausgeführt und Sie erhalten eine Fehlermeldung - genau wie in Ihrem Fall. Dies kann auch zu Speicherlecks führen.

Deshalb 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 - dieses ist ein Antimuster, kann es aber sein ausreichend in Ihrem Fall (genau wie bei Formik Implementierung).

Eine ähnliche Diskussion zu 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 zu useRef, das sich wie eine statische Eigenschaft einer Klasse verhält, dh, wenn sich der Wert ändert, wird die Komponente nicht erneut gerendert:

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

69
Tomasz Mularczyk 1 Sept. 2019 im 15:29

Die freundlichen Mitarbeiter von React empfehlen, Ihre Abrufe / Versprechen einzuwickeln ein stornierbares Versprechen. Obwohl in dieser Dokumentation keine Empfehlung enthalten ist, den Code von der Klasse oder Funktion mit dem Abruf 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 in componentWillUnmount() entsorgt oder storniert werden. Gemäß React können Sie cancel() für das verpackte Versprechen in componentWillUnmount aufrufen, 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.

22
haleonj 30 Sept. 2018 im 16:32

Sie können AbortController verwenden, um eine Abrufanforderung abzubrechen.

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>
20
OZZIE 20 Dez. 2019 im 12:57

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);
});
10
Ben Yitzhaki 26 Dez. 2018 im 15:15

Der Kern dieser Warnung besteht darin, dass Ihre Komponente einen Verweis darauf hat, 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 hält), schlägt die Reaktionswebsite unter Verwendung eines optionalen Versprechens; Dieser Code scheint jedoch auch Ihr Objekt am Leben zu erhalten.

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.
    }
}
3
Anthony Wieser 3 Mai 2018 im 11:31

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

2
Sasha Kos 18 Apr. 2018 im 18:40

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);
  }
}
2
Sasha Kos 29 Okt. 2019 im 16:20

Zusätzlich zu den Beispielen für stornierbare Versprechen in der akzeptierten Lösung kann es nützlich sein, einen useAsyncCallback -Haken zu haben, der einen Anforderungsrückruf umschließt und ein stornierbares Versprechen zurückgibt. Die Idee ist dieselbe, aber mit einem Haken, der genau wie ein normaler useCallback funktioniert. 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
}
0
Thomas Jgenti 2 Dez. 2019 im 08:37

Ich glaube, ich habe einen Weg gefunden, um das zu umgehen. Das Problem ist nicht so sehr das Abrufen selbst, sondern der setState nach dem Löschen der Komponente. Die Lösung bestand also darin, this.state.isMounted als false zu setzen und dann bei componentWillMount auf true zu ändern und in componentWillUnmount erneut auf false zu setzen. Dann einfach if(this.state.isMounted) den setState innerhalb des Abrufs. 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,
    })
  }
-2
João Belo 18 Apr. 2018 im 19:06