Es kann keine Aktualisierung des Reaktionsstatus für eine nicht gemountete Komponente durchgeführt werden

125

Problem

Ich schreibe eine Anwendung in React und war nicht in der Lage ein Super häufigen Fehler zu vermeiden, die riefen setState(...)nach componentWillUnmount(...).

Ich habe mir meinen Code sehr genau angesehen und versucht, einige Schutzklauseln einzuführen, aber das Problem blieb bestehen und ich beobachte die Warnung immer noch.

Daher habe ich zwei Fragen:

  1. Wie kann ich anhand der Stapelverfolgung herausfinden , welche bestimmte Komponente und jeder Ereignishandler oder Lebenszyklus-Hook für die Regelverletzung verantwortlich ist?
  2. Nun, wie man das Problem selbst behebt, da mein Code unter Berücksichtigung dieser Gefahr geschrieben wurde und bereits versucht, dies zu verhindern, aber einige zugrunde liegende Komponenten immer noch die Warnung generieren.

Browserkonsole

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

Geben Sie hier die Bildbeschreibung ein

Code

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

Update 1: Drosselbare Funktion abbrechen (immer noch kein Glück)

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;
Igor Soloydenko
quelle
Bleibt das Problem bestehen, wenn Sie die Listener zum Hinzufügen und Entfernen auskommentieren?
ic3b3rg
@ ic3b3rg das Problem verschwindet, wenn es keinen Ereignis-Abhörcode gibt
Igor Soloydenko
ok, hast du den Vorschlag versucht, this.setDivSizeThrottleable.cancel()anstelle der this.isComponentMountedWache zu tun ?
ic3b3rg
1
@ ic3b3rg Immer noch die gleiche Laufzeitwarnung.
Igor Soloydenko
Mögliches Duplikat von React - setState () auf nicht gemounteter Komponente
Emile Bergeron

Antworten:

68

Hier ist eine React Hooks- spezifische Lösung für

Error

Warnung: Es kann keine Aktualisierung des Reaktionsstatus für eine nicht gemountete Komponente durchgeführt werden.

Lösung

Sie können let isMounted = trueinnen deklarieren useEffect, was im Bereinigungsrückruf geändert wird, sobald die Komponente nicht mehr gemountet ist. Vor Statusaktualisierungen überprüfen Sie diese Variable jetzt bedingt:

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

Erweiterung: Benutzerdefinierter useAsyncHaken

Wir können die gesamte Boilerplate in einen benutzerdefinierten Hook kapseln, der nur weiß, wie man mit asynchronen Funktionen umgeht und diese automatisch abbricht, falls die Komponente zuvor die Bereitstellung aufhebt:

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

ford04
quelle
1
Deine Tricks funktionieren! Ich frage mich, was die Magie dahinter steckt.
Niyongabo
1
Wir nutzen hier die integrierte Funktion zur Bereinigung von Effekten, die ausgeführt wird, wenn sich Abhängigkeiten ändern, und in beiden Fällen, wenn die Bereitstellung der Komponente aufgehoben wird. Dies ist also der perfekte Ort, um eine isMountedFlagge umzuschalten false, auf die über den Bereich zum Schließen des Rückrufs für umgebende Effekte zugegriffen werden kann. Sie können sich die Bereinigungsfunktion als zu ihrem entsprechenden Effekt gehörend vorstellen.
ford04
1
es ergibt Sinn! Ich bin mit Ihrer Antwort zufrieden. Ich habe daraus gelernt.
Niyongabo
heiliger Rauch ... dieses isMountedDing funktioniert. Ich bin auf 10.4.7React -Testing-Lib und Formik ^2.1.4. Das fühlt sich an wie ein totaler Hack und das Ergebnis von etwas mit Formik.
Phil Lucks
1
@ VictorMolina Nein, das wäre sicherlich übertrieben. Betrachten Sie diese Technik für Komponenten a), die asynchrone Operationen wie fetchin useEffectund b) verwenden, die nicht stabil sind, dh möglicherweise nicht bereitgestellt werden, bevor das asynchrone Ergebnis zurückkehrt und bereit ist, als Status festgelegt zu werden.
ford04
83

Entfernen - Bei einer nicht gemounteten Komponentenwarnung kann keine Aktualisierung des Reaktionsstatus durchgeführt werden. Verwenden Sie die componentDidMount-Methode unter einer Bedingung und machen Sie diese Bedingung für die componentWillUnmount-Methode falsch. Zum Beispiel : -

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() {
    ...
  }
}
Vinod
quelle
2
Das hat funktioniert, aber warum sollte das funktionieren? Was genau verursacht diesen Fehler? und wie dies behoben wurde: |
Abhinav
Es funktioniert gut. Der sich wiederholende Aufruf der setState-Methode wird gestoppt, da der Wert _isMounted vor dem Aufruf von setState überprüft und anschließend in componentWillUnmount () erneut auf false zurückgesetzt wird. Ich denke, so funktioniert es.
Abhishek
7
Verwenden Sie für die const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
Hakenkomponente Folgendes
@ x-magix Dafür brauchst du eigentlich keine Referenz, du kannst einfach eine lokale Variable verwenden, auf der die Rückgabefunktion schließen kann.
Mordechai
@Abhinav Meine beste Vermutung, warum dies funktioniert, ist, dass _isMountedes nicht von React verwaltet wird (im Gegensatz statezu React) und daher nicht der Rendering-Pipeline von React unterliegt . Das Problem ist, dass React beim Aufheben der Bereitstellung einer Komponente alle Aufrufe von in setState()die Warteschlange stellt (was ein erneutes Rendern auslösen würde). Daher wird der Status nie aktualisiert
Lightfire228
26

Wenn die oben genannten Lösungen nicht funktionieren, versuchen Sie dies und es funktioniert für mich:

componentWillUnmount() {
    // fix Warning: Can't perform a React state update on an unmounted component
    this.setState = (state,callback)=>{
        return;
    };
}
May'st
quelle
Danke, es funktioniert bei mir. Kann mir jemand diesen Code erklären?
Badri Paudel
@BadriPaudel null zurück , wenn escapse Komponente, wird es nicht alle Daten halten länger im Speicher
may'st
Vielen Dank dafür!
Tushar Gupta
was zurückgeben? einfach einfügen wie es ist?
plus
10

Ich hatte diese Warnung möglicherweise wegen des Aufrufs setStatevon einem Effekt Haken (Dies in diesen 3 diskutiert Themen verknüpft zusammen ).

Durch das Aktualisieren der Reaktionsversion wurde die Warnung entfernt.

Peter Lamberg
quelle
5

versuchen Wechsel setDivSizeThrottleablezu

this.setDivSizeThrottleable = throttle(
  () => {
    if (this.isComponentMounted) {
      this.setState({
        pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
      });
    }
  },
  500,
  { leading: false, trailing: true }
);
ic3b3rg
quelle
Ich habe es versucht. Jetzt sehe ich ständig die Warnung, die ich nur von Zeit zu Zeit beim Ändern der Fenstergröße beobachtete, bevor ich diese Änderung vornahm. ¯_ (ツ) _ / ¯ Vielen Dank, dass Sie dies versucht haben.
Igor Soloydenko
3

Ich weiß, dass Sie keinen Verlauf verwenden, aber in meinem Fall habe ich den useHistoryHook von React Router DOM verwendet, der die Bereitstellung der Komponente aufhebt, bevor der Status in meinem React Context Provider beibehalten wird.

Um dieses Problem zu beheben, habe ich den Haken verwendet, der withRouterdie Komponente in meinem Fall export default withRouter(Login)und innerhalb der Komponente verschachtelt const Login = props => { ...; props.history.push("/dashboard"); .... Ich habe auch den anderen props.history.pushaus der Komponente entfernt, zB if(authorization.token) return props.history.push('/dashboard')weil dies eine Schleife verursacht, weil der authorizationZustand.

Eine Alternative, um ein neues Element in den Verlauf zu verschieben .

Walter
quelle
2

Wenn Sie Daten von Axios abrufen und der Fehler weiterhin auftritt, wickeln Sie den Setter einfach in die Bedingung ein

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;
    };
}, []);
Drew Cordano
quelle
1

Bearbeiten: Ich habe gerade festgestellt, dass die Warnung auf eine Komponente namens verweist TextLayerInternal. Das ist wahrscheinlich, wo Ihr Fehler ist. Der Rest ist immer noch relevant, kann Ihr Problem jedoch möglicherweise nicht beheben.

1) Es ist schwierig, die Instanz einer Komponente für diese Warnung zu ermitteln. Es sieht so aus, als gäbe es einige Diskussionen, um dies in React zu verbessern, aber es gibt derzeit keine einfache Möglichkeit, dies zu tun. Der Grund, warum es noch nicht erstellt wurde, liegt wahrscheinlich darin, dass erwartet wird, dass Komponenten so geschrieben werden, dass setState nach dem Aufheben der Bereitstellung nicht möglich ist, unabhängig vom Status der Komponente. Für das React-Team liegt das Problem immer im Komponentencode und nicht in der Komponenteninstanz, weshalb Sie den Namen des Komponententyps erhalten.

Diese Antwort mag unbefriedigend sein, aber ich denke, ich kann Ihr Problem beheben.

2) Die gedrosselte Funktion von Lodashes hat eine cancelMethode. Rufen Sie cancelan componentWillUnmountund lassen Sie das fallen isComponentMounted. Abbrechen ist eher "idiomatisch" als die Einführung einer neuen Eigenschaft.

R Esmond
quelle
Das Problem ist, ich kontrolliere nicht direkt TextLayerInternal. Daher weiß ich nicht, "wer schuld ist der setState()Anruf". Ich werde das cancelgemäß Ihrem Rat versuchen und sehen, wie es geht,
Igor Soloydenko
Leider sehe ich immer noch die Warnung. Bitte überprüfen Sie den Code im Abschnitt Update 1, um sicherzustellen, dass ich die Dinge richtig mache.
Igor Soloydenko
1

Ich hatte ein ähnliches Problem, danke @ ford04 hat mir geholfen.

Es ist jedoch ein anderer Fehler aufgetreten.

NB. Ich benutze ReactJS-Hooks

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.

Was verursacht den Fehler?

import {useHistory} from 'react-router-dom'

const History = useHistory()
if (true) {
  history.push('/new-route');
}
return (
  <>
    <render component />
  </>
)

Dies konnte nicht funktionieren, da trotz der Umleitung auf eine neue Seite alle Status und Requisiten auf dem Dom bearbeitet wurden oder das Rendern auf die vorherige Seite nicht gestoppt wurde.

Welche Lösung habe ich gefunden

import {Redirect} from 'react-router-dom'

if (true) {
  return <redirect to="/new-route" />
}
return (
  <>
    <render component />
  </>
)
Niyongabo
quelle
0

Ich hatte ein ähnliches Problem und löste es:

Ich habe den Benutzer automatisch angemeldet, indem ich eine Aktion für Redux ausgelöst habe (Authentifizierungstoken auf Redux-Status setzen).

und dann habe ich versucht, eine Nachricht mit this.setState ({succ_message: "...") in meiner Komponente anzuzeigen.

Komponente sah leer aus mit dem gleichen Fehler auf der Konsole: "nicht gemountete Komponente" .. "Speicherverlust" usw.

Nachdem ich Walters Antwort in diesem Thread gelesen habe

Ich habe festgestellt, dass in der Routing-Tabelle meiner Anwendung die Route meiner Komponente nicht gültig war, wenn der Benutzer angemeldet ist:

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

Ich habe die Route sichtbar gemacht, ob der Token existiert oder nicht.

Beemaster
quelle
0

Basierend auf der Antwort von @ ford04 ist hier dasselbe in einer Methode eingekapselt:

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

Verwendung:

const MyComponent : FC<{}> = (props) => {
    const [ asyncProp , setAsyncProp ] = useState( '' ) ;
    useEffectAsync( async ( isMounted ) =>
    {
        const someAsyncProp = await ... ;
        if ( isMounted() )
             setAsyncProp( someAsyncProp ) ;
    });
    return <div> ... ;
} ;
Nicolas
quelle