Nie można przeprowadzić aktualizacji stanu React na niezmontowanym komponencie

143

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:

  1. 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?
  2. 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

wprowadź opis obrazu tutaj

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;
Igor Soloydenko
źródło
Czy problem będzie się powtarzał, jeśli skomentujesz dodawanie i usuwanie słuchaczy?
ic3b3rg
@ ic3b3rg problem znika, jeśli nie ma kodu nasłuchującego zdarzenia
Igor Soloydenko
ok, czy próbowałeś z sugestii zrobić this.setDivSizeThrottleable.cancel()zamiast this.isComponentMountedstrażnika?
ic3b3rg
1
@ ic3b3rg Wciąż to samo ostrzeżenie w czasie wykonywania.
Igor Soloydenko
Możliwy duplikat React - setState () na niezmontowanym komponencie
Emile Bergeron

Odpowiedzi:

95

Oto rozwiązanie specyficzne dla React hooków dla

Błąd

Ostrzeżenie: nie można przeprowadzić aktualizacji stanu React na niezmontowanym komponencie.

Rozwiązanie

Możesz zadeklarować let isMounted = truewewnątrz useEffect, 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
});

Rozszerzenie: niestandardowy useAsynchak

Moż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]);
}

ford04
źródło
2
Twoje sztuczki działają! Zastanawiam się, co kryje się za magią?
Niyongabo
1
Wykorzystujemy tutaj wbudowaną funkcję czyszczenia efektów , która działa, gdy zmieniają się zależności oraz w każdym przypadku, gdy komponent się odłącza. Jest to więc idealne miejsce na przełączenie isMountedflagi, do falsektó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.
ford04
1
to ma sens! Jestem zadowolony z Twojej odpowiedzi. Nauczyłem się z tego.
Niyongabo
1
@VictorMolina Nie, to z pewnością byłaby przesada. Rozważ tę technikę dla komponentów a) używając operacji asynchronicznych, takich jak fetchin useEffectib), które nie są stabilne, tj. Mogą zostać odłączone przed zwróceniem wyniku asynchronicznego i są gotowe do ustawienia jako stan.
ford 04
1
stackoverflow.com/a/63213676 i medium.com/better-programming/ ... były interesujące, ale ostatecznie twoja odpowiedź pomogła mi w uruchomieniu mojej. Dzięki!
Ryan
87

Aby usunąć - nie można wykonać aktualizacji stanu React na ostrzeżeniu o niezamontowanym komponencie, użyj metody componentDidMount pod warunkiem i zrób fałsz w metodzie componentWillUnmount. Na przykład : -

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
źródło
3
To zadziałało, ale dlaczego to powinno działać? Co dokładnie powoduje ten błąd? i jak to naprawiło: |
Abhinav
Działa dobrze. Zatrzymuje powtarzające się wywołania metody setState, ponieważ sprawdza poprawność wartości _isMounted przed wywołaniem setState, a następnie ponownie resetuje do wartości false w componentWillUnmount (). Myślę, że tak to działa.
Abhishek
8
dla elementu haczyka użyj tego:const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
x-magix
@ x-magix Naprawdę nie potrzebujesz do tego ref, możesz po prostu użyć lokalnej zmiennej, którą funkcja powrotu może zamknąć.
Mordechai
@Abhinav Moim najlepszym przypuszczeniem, dlaczego to działa, jest to, że _isMountednie jest zarządzany przez React (w przeciwieństwie state) i dlatego nie podlega potokowi renderowania Reacta . Problem polega na tym, że gdy komponent jest ustawiony na odmontowanie, React usuwa z kolejki wszelkie wywołania setState()(co spowodowałoby „ponowne renderowanie”); dlatego stan nigdy nie jest aktualizowany
Lightfire228
35

Jeś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;
    };
}
May'Habit
źródło
1
Dzięki, to działa dla mnie. Czy ktoś może mi wyjaśnić ten fragment kodu?
Badri Paudel
@BadriPaudel zwraca wartość null, gdy składnik escapse, nie będzie już przechowywać żadnych danych w pamięci
May'Habit
Dziękuję bardzo za to!
Tushar Gupta
co zwrócić? po prostu wkleić tak, jak jest?
plus
11

Otrzymałem to ostrzeżenie prawdopodobnie z powodu wywołania setStatez 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.

Peter Lamberg
źródło
5

spróbuj zmienić setDivSizeThrottleablena

this.setDivSizeThrottleable = throttle(
  () => {
    if (this.isComponentMounted) {
      this.setState({
        pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
      });
    }
  },
  500,
  { leading: false, trailing: true }
);
ic3b3rg
źródło
Spróbowałem. Teraz konsekwentnie widzę ostrzeżenie, które obserwowałem tylko od czasu do czasu przy zmianie rozmiaru okna przed dokonaniem tej zmiany. ¯_ (ツ) _ / ¯ Dzięki za wypróbowanie tego.
Igor Soloydenko
5

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 withRouterzagnieżdżającego komponent, w moim przypadku export default withRouter(Login)i wewnątrz komponentu const Login = props => { ...; props.history.push("/dashboard"); .... Usunąłem też drugą props.history.pushz komponentu np. if(authorization.token) return props.history.push('/dashboard')Bo to powoduje pętlę, bo authorizationstan.

Alternatywa dla wypchnięcia nowego elementu do historii .

Walter
źródło
2

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;
    };
}, []);
Drew Cordano
źródło
2

Istnieje dość powszechny hak, useIsMountedktó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); }
    })
  });
  ...
}
sfletche
źródło
1

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 cancelmetodę. Zadzwoń cancelw componentWillUnmounti rowu isComponentMounted. Anulowanie jest bardziej „idiomatycznym” Reagowaniem niż wprowadzaniem nowej właściwości.

R Esmond
źródło
Problem w tym, że nie kontroluję bezpośrednio TextLayerInternal. Dlatego nie wiem, „kto jest winą setState()wezwania”. Spróbuję cancelzgodnie z twoją radą i zobaczę, jak pójdzie,
Igor Soloydenko
Niestety nadal widzę ostrzeżenie. Sprawdź kod w sekcji Aktualizacja 1, aby upewnić się, że robię wszystko we właściwy sposób.
Igor Soloydenko
1

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.

Co powoduje błąd?

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ę.

Jakie rozwiązanie znalazłem

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

if (true) {
  return <redirect to="/new-route" />
}
return (
  <>
    <render component />
  </>
)
Niyongabo
źródło
1

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.

coder9833idls
źródło
Czy chcesz powiedzieć, że componentDidMount()można zadzwonić dwukrotnie bez pośredniego componentWillUnmount()połączenia? Nie sądzę, żeby to było możliwe.
Alexis Wilke
1
Nie, mówię, że nie jest wywoływana dwukrotnie, dlatego strona nie przetwarza kodu wewnątrz 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.
coder9833idls
0

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.

pszczelarz
źródło
0

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> ... ;
} ;
Nicolas
źródło
0

Zainspirowany zaakceptowaną odpowiedzią @ ford04, podjąłem się jeszcze lepiej, zamiast używać useEffectinside, useAsyncstworzyć nową funkcję, która zwraca callback dla componentWillUnmount:

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)
    },[])

guneetgstar
źródło
Nie polecałbym polegać na isMountedzmiennej lokalnej , ale zamiast tego uczynić ją stanem (za pomocą useStatepodpięcia).
Igor Soloydenko
Jakie to ma znaczenie? Przynajmniej nie przychodzi mi do głowy żadne inne zachowanie.
guneetgstar