Problem
Piszę aplikację w React i nie był w stanie uniknąć super Częstym błędem, który dzwoni setState(...)
po componentWillUnmount(...)
.
Bardzo uważnie przyjrzałem się mojemu kodowi i próbowałem wprowadzić pewne klauzule ochronne, ale problem nie ustąpił i nadal przestrzegam ostrzeżenia.
Dlatego mam dwa pytania:
- Jak mogę dowiedzieć się na podstawie śladu stosu , który konkretny składnik i program obsługi zdarzeń lub punkt zaczepienia cyklu życia jest odpowiedzialny za naruszenie reguły?
- Cóż, jak rozwiązać sam problem, ponieważ mój kod został napisany z myślą o tej pułapce i już próbuje temu zapobiec, ale niektóre podstawowe komponenty nadal generują ostrzeżenie.
Konsola przeglądarki
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
Kod
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>
);
}
Aktualizacja 1: Anuluj funkcję przepustnicy (nadal nie ma szczęścia)
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
źródło
źródło
this.setDivSizeThrottleable.cancel()
zamiastthis.isComponentMounted
strażnika?Odpowiedzi:
Oto rozwiązanie specyficzne dla React hooków dla
Błąd
Rozwiązanie
Możesz zadeklarować
let isMounted = true
wewnątrzuseEffect
, co zostanie zmienione w wywołaniu zwrotnym czyszczenia, gdy tylko komponent zostanie odmontowany. Przed aktualizacjami stanu możesz teraz sprawdzać tę zmienną warunkowo: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 });
Pokaż fragment kodu
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>
Rozszerzenie: niestandardowy
useAsync
hakMożemy zamknąć wszystkie standardowe elementy w niestandardowym hooku, który po prostu wie, jak radzić sobie z funkcjami asynchronicznymi i automatycznie przerywać je w przypadku odmontowania komponentu wcześniej:
function useAsync(asyncFn, onSuccess) { useEffect(() => { let isMounted = true; asyncFn().then(data => { if (isMounted) onSuccess(data); }); return () => { isMounted = false }; }, [asyncFn, onSuccess]); }
Pokaż fragment kodu
// 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>
źródło
isMounted
flagi, dofalse
której można uzyskać dostęp z otaczającego zakresu zamknięcia wywołania zwrotnego efektu. Możesz myśleć o funkcji czyszczenia jako o przynależności do odpowiedniego efektu.fetch
inuseEffect
ib), które nie są stabilne, tj. Mogą zostać odłączone przed zwróceniem wyniku asynchronicznego i są gotowe do ustawienia jako stan.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() { ... } }
źródło
const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
_isMounted
nie jest zarządzany przez React (w przeciwieństwiestate
) i dlatego nie podlega potokowi renderowania Reacta . Problem polega na tym, że gdy komponent jest ustawiony na odmontowanie, React usuwa z kolejki wszelkie wywołaniasetState()
(co spowodowałoby „ponowne renderowanie”); dlatego stan nigdy nie jest aktualizowanyJeśli powyższe rozwiązania nie działają, wypróbuj to i działa u mnie:
componentWillUnmount() { // fix Warning: Can't perform a React state update on an unmounted component this.setState = (state,callback)=>{ return; }; }
źródło
Otrzymałem to ostrzeżenie prawdopodobnie z powodu wywołania
setState
z haka efektu (jest to omówione w tych 3 połączonych ze sobą sprawach ).W każdym razie aktualizacja wersji Reagowania usunęła ostrzeżenie.
źródło
spróbuj zmienić
setDivSizeThrottleable
nathis.setDivSizeThrottleable = throttle( () => { if (this.isComponentMounted) { this.setState({ pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5, }); } }, 500, { leading: false, trailing: true } );
źródło
Wiem, że nie używasz historii, ale w moim przypadku korzystałem z
useHistory
hooka z React Router DOM, który odmontowuje komponent, zanim stan zostanie utrwalony w moim React Context Provider.Aby rozwiązać ten problem, użyłem haka
withRouter
zagnieżdżającego komponent, w moim przypadkuexport default withRouter(Login)
i wewnątrz komponentuconst Login = props => { ...; props.history.push("/dashboard"); ...
. Usunąłem też drugąprops.history.push
z komponentu np.if(authorization.token) return props.history.push('/dashboard')
Bo to powoduje pętlę, boauthorization
stan.Alternatywa dla wypchnięcia nowego elementu do historii .
źródło
Jeśli pobierasz dane z axios, a błąd nadal występuje, po prostu zawiń setter wewnątrz warunku
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; }; }, []);
źródło
Istnieje dość powszechny hak,
useIsMounted
który rozwiązuje ten problem (w przypadku elementów funkcjonalnych) ...import { useRef, useEffect } from 'react'; export function useIsMounted() { const isMounted = useRef(false); useEffect(() => { isMounted.current = true; return () => isMounted.current = false; }, []); return isMounted; }
następnie w komponencie funkcjonalnym
function Book() { const isMounted = useIsMounted(); ... useEffect(() => { asyncOperation().then(data => { if (isMounted.current) { setState(data); } }) }); ... }
źródło
Edycja: właśnie zdałem sobie sprawę, że ostrzeżenie odnosi się do komponentu o nazwie
TextLayerInternal
. To prawdopodobnie tam, gdzie jest twój błąd. Reszta jest nadal aktualna, ale może nie rozwiązać problemu.1) Uzyskanie instancji składnika dla tego ostrzeżenia jest trudne. Wygląda na to, że toczy się dyskusja, aby to poprawić w Reakcie, ale obecnie nie ma łatwego sposobu, aby to zrobić. Podejrzewam, że powodem tego, że nie został jeszcze zbudowany, jest prawdopodobnie to, że komponenty mają być napisane w taki sposób, że setState po odmontowaniu nie jest możliwe bez względu na stan komponentu. Jeśli chodzi o zespół Reacta, problem zawsze tkwi w kodzie komponentu, a nie w instancji komponentu, dlatego otrzymujesz nazwę typu komponentu.
Ta odpowiedź może być niezadowalająca, ale myślę, że mogę rozwiązać twój problem.
2) Ograniczona funkcja Lodasha ma
cancel
metodę. Zadzwońcancel
wcomponentWillUnmount
i rowuisComponentMounted
. Anulowanie jest bardziej „idiomatycznym” Reagowaniem niż wprowadzaniem nowej właściwości.źródło
TextLayerInternal
. Dlatego nie wiem, „kto jest winąsetState()
wezwania”. Spróbujęcancel
zgodnie z twoją radą i zobaczę, jak pójdzie,Miałem podobny problem, dzięki @ ford04 pomógł mi.
Jednak wystąpił inny błąd.
NB. Używam hooków ReactJS
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 /> </> )
To nie może zadziałać, ponieważ pomimo przekierowania na nową stronę, cały stan i właściwości są manipulowane w dom lub po prostu renderowanie na poprzednią stronę nie zatrzymało się.
import {Redirect} from 'react-router-dom' if (true) { return <redirect to="/new-route" /> } return ( <> <render component /> </> )
źródło
W zależności od tego, jak otworzysz swoją stronę internetową, możesz nie powodować montowania. Na przykład przy użyciu
<Link/>
powrotu do strony, która została już zamontowana w wirtualnym modelu DOM, więc przechwytuje się wymaganie danych z cyklu życia componentDidMount.źródło
componentDidMount()
można zadzwonić dwukrotnie bez pośredniegocomponentWillUnmount()
połączenia? Nie sądzę, żeby to było możliwe.componentDidMount()
podczas korzystania z<Link/>
. Używam Redux do tych problemów i przechowuję dane strony w sklepie Reducera, więc i tak nie muszę ponownie ładować strony.Miałem podobny problem i rozwiązałem go:
Automatycznie logowałem użytkownika, wysyłając akcję na redux (umieszczenie tokena uwierzytelniającego w stanie redux)
a potem próbowałem wyświetlić komunikat z this.setState ({succ_message: "...") w moim komponencie.
Komponent wyglądał na pusty z tym samym błędem na konsoli: „odmontowany komponent” .. „wyciek pamięci” itp.
Po przeczytaniu odpowiedzi Waltera w tym wątku
Zauważyłem, że w tabeli routingu mojej aplikacji trasa mojego komponentu nie była poprawna, jeśli użytkownik jest zalogowany:
{!this.props.user.token && <div> <Route path="/register/:type" exact component={MyComp} /> </div> }
Sprawiłem, że Trasa jest widoczna, czy token istnieje, czy nie.
źródło
Na podstawie odpowiedzi @ ford04, oto ta sama metoda ujęta w metodzie:
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 ); }
Stosowanie:
const MyComponent : FC<{}> = (props) => { const [ asyncProp , setAsyncProp ] = useState( '' ) ; useEffectAsync( async ( isMounted ) => { const someAsyncProp = await ... ; if ( isMounted() ) setAsyncProp( someAsyncProp ) ; }); return <div> ... ; } ;
źródło
Zainspirowany zaakceptowaną odpowiedzią @ ford04, podjąłem się jeszcze lepiej, zamiast używać
useEffect
inside,useAsync
stworzyć nową funkcję, która zwraca callback dlacomponentWillUnmount
:function asyncRequest(asyncRequest, onSuccess, onError, onComplete) { let isMounted=true asyncRequest().then((data => isMounted ? onSuccess(data):null)).catch(onError).finally(onComplete) return () => {isMounted=false} } ... useEffect(()=>{ return asyncRequest(()=>someAsyncTask(arg), response=> { setSomeState(response) },onError, onComplete) },[])
źródło
isMounted
zmiennej lokalnej , ale zamiast tego uczynić ją stanem (za pomocąuseState
podpięcia).