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:
- Wie kann ich anhand der Stapelverfolgung herausfinden , welche bestimmte Komponente und jeder Ereignishandler oder Lebenszyklus-Hook für die Regelverletzung verantwortlich ist?
- 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
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;
javascript
reactjs
typescript
lodash
setstate
Igor Soloydenko
quelle
quelle
this.setDivSizeThrottleable.cancel()
anstelle derthis.isComponentMounted
Wache zu tun ?Antworten:
Hier ist eine React Hooks- spezifische Lösung für
Error
Lösung
Sie können
let isMounted = true
innen deklarierenuseEffect
, 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 });
Code-Snippet anzeigen
const Parent = () => { const [mounted, setMounted] = useState(true); return ( <div> Parent: <button onClick={() => setMounted(!mounted)}> {mounted ? "Unmount" : "Mount"} Child </button> {mounted && <Child />} <p> Unmount Child, while it is still loading. It won't set state later on, so no error is triggered. </p> </div> ); }; const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useEffect(() => { let isMounted = true; // note this mounted flag fetchData(); return () => { isMounted = false; }; // use effect cleanup to set flag false, if unmounted // simulate some Web API fetching function fetchData() { setTimeout(() => { // drop "if (isMounted)" to trigger error again if (isMounted) setState("data fetched"); }, 4000); } }, []); return <div>Child: {state}</div>; }; ReactDOM.render(<Parent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>
Erweiterung: Benutzerdefinierter
useAsync
HakenWir 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]); }
Code-Snippet anzeigen
// use async operation with automatic abortion on unmount function useAsync(asyncFn, onSuccess) { useEffect(() => { let isMounted = true; asyncFn().then(data => { if (isMounted) onSuccess(data); }); return () => { isMounted = false; }; }, [asyncFn, onSuccess]); } const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useAsync(delay, setState); return <div>Child: {state}</div>; }; const Parent = () => { const [mounted, setMounted] = useState(true); return ( <div> Parent: <button onClick={() => setMounted(!mounted)}> {mounted ? "Unmount" : "Mount"} Child </button> {mounted && <Child />} <p> Unmount Child, while it is still loading. It won't set state later on, so no error is triggered. </p> </div> ); }; const delay = () => new Promise(resolve => setTimeout(() => resolve("data fetched"), 4000)); ReactDOM.render(<Parent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script> <div id="root"></div> <script>var { useReducer, useEffect, useState, useRef } = React</script>
quelle
isMounted
Flagge umzuschaltenfalse
, 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.isMounted
Ding funktioniert. Ich bin auf10.4.7
React -Testing-Lib und Formik^2.1.4
. Das fühlt sich an wie ein totaler Hack und das Ergebnis von etwas mit Formik.fetch
inuseEffect
und 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.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() { ... } }
quelle
const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
_isMounted
es nicht von React verwaltet wird (im Gegensatzstate
zu React) und daher nicht der Rendering-Pipeline von React unterliegt . Das Problem ist, dass React beim Aufheben der Bereitstellung einer Komponente alle Aufrufe von insetState()
die Warteschlange stellt (was ein erneutes Rendern auslösen würde). Daher wird der Status nie aktualisiertWenn 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; }; }
quelle
Ich hatte diese Warnung möglicherweise wegen des Aufrufs
setState
von einem Effekt Haken (Dies in diesen 3 diskutiert Themen verknüpft zusammen ).Durch das Aktualisieren der Reaktionsversion wurde die Warnung entfernt.
quelle
versuchen Wechsel
setDivSizeThrottleable
zuthis.setDivSizeThrottleable = throttle( () => { if (this.isComponentMounted) { this.setState({ pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5, }); } }, 500, { leading: false, trailing: true } );
quelle
Ich weiß, dass Sie keinen Verlauf verwenden, aber in meinem Fall habe ich den
useHistory
Hook 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
withRouter
die Komponente in meinem Fallexport default withRouter(Login)
und innerhalb der Komponente verschachteltconst Login = props => { ...; props.history.push("/dashboard"); ...
. Ich habe auch den anderenprops.history.push
aus der Komponente entfernt, zBif(authorization.token) return props.history.push('/dashboard')
weil dies eine Schleife verursacht, weil derauthorization
Zustand.Eine Alternative, um ein neues Element in den Verlauf zu verschieben .
quelle
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; }; }, []);
quelle
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
cancel
Methode. Rufen Siecancel
ancomponentWillUnmount
und lassen Sie das fallenisComponentMounted
. Abbrechen ist eher "idiomatisch" als die Einführung einer neuen Eigenschaft.quelle
TextLayerInternal
. Daher weiß ich nicht, "wer schuld ist dersetState()
Anruf". Ich werde dascancel
gemäß Ihrem Rat versuchen und sehen, wie es geht,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.
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.
import {Redirect} from 'react-router-dom' if (true) { return <redirect to="/new-route" /> } return ( <> <render component /> </> )
quelle
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.
quelle
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> ... ; } ;
quelle