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;

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

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


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() {
    ...
  }
}

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

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


5

spróbuj zmienić setDivSizeThrottleablena

this.setDivSizeThrottleable = throttle(
  () => {
    if (this.isComponentMounted) {
      this.setState({
        pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
      });
    }
  },
  500,
  { leading: false, trailing: true }
);

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 .


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

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

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.


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 />
  </>
)

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.


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.


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

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


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.