Bağlanmayan bir bileşende React durum güncellemesi gerçekleştirilemez


125

Sorun

React'te bir uygulama yazıyorum ve daha setState(...)sonra arayan süper yaygın bir tuzaktan kaçınamadım componentWillUnmount(...).

Koduma çok dikkatli baktım ve bazı koruma maddeleri koymaya çalıştım, ancak sorun devam etti ve hala uyarıyı gözlemliyorum.

Bu nedenle, iki sorum var:

  1. Yığın izlemeden , kural ihlalinden hangi bileşenin ve olay işleyicisinin veya yaşam döngüsü kancasının sorumlu olduğunu nasıl anlayabilirim ?
  2. Peki, sorunun kendisi nasıl çözülür, çünkü kodum bu tuzak düşünülerek yazılmıştı ve zaten onu önlemeye çalışıyor, ancak bazı temel bileşenler hala uyarıyı oluşturuyor.

Tarayıcı konsolu

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

görüntü açıklamasını buraya girin

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

Güncelleme 1: Kısılabilir işlevi iptal edin (hala şans yok)

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;

Ekle ve kaldır dinleyicileri hakkında yorum yaparsanız sorun devam ediyor mu?
ic3b3rg

@ ic3b3rg olay dinleme kodu yoksa sorun ortadan kalkar
Igor Soloydenko

tamam, bekçi this.setDivSizeThrottleable.cancel()yerine yapmayı önermeyi denedin this.isComponentMountedmi?
ic3b3rg

1
@ ic3b3rg Hala aynı çalışma zamanı uyarısı.
Igor Soloydenko

Yanıtlar:


68

İşte React Hooks'a özel bir çözüm

Hata

Uyarı: Bağlanmayan bir bileşende React durum güncellemesi gerçekleştirilemez.

Çözüm

Bileşen kaldırılır kaldırılmaz temizleme geri aramasında değiştirilecek olan let isMounted = trueiçeriyi bildirebilirsiniz useEffect. Durum güncellemelerinden önce, şimdi bu değişkeni koşullu olarak kontrol edersiniz:

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

Uzatma: Özel useAsyncKanca

Tüm kazan plakasını, bileşenin daha önce ayrılması durumunda zaman uyumsuz işlevlerin nasıl üstesinden gelineceğini ve otomatik olarak durdurulacağını bilen özel bir Kanca içine yerleştirebiliriz:

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


1
numaraların işe yarıyor! Merak ediyorum arkasındaki sihir nedir?
Niyongabo

1
Burada, bağımlılıklar değiştiğinde ve her iki durumda da bileşen ayrıldığında çalışan yerleşik efekt temizleme özelliğinden yararlanıyoruz . Dolayısıyla burası, çevreleyen geri arama kapatma kapsamından erişilebilen bir isMountedbayrağı değiştirmek için mükemmel bir yerdir false. Temizleme işlevinin , karşılık gelen etkiye ait olduğunu düşünebilirsiniz .
ford04

1
mantıklı! Cevabınızdan memnunum. Ben ondan öğrendim.
Niyongabo

kutsal dumanlar ... bu isMountedşey çalışıyor. 10.4.7React -testing-lib ve formik kullanıyorum ^2.1.4. Bu tam bir hack ve Formik'teki bir şeyin sonucu gibi geliyor.
Phil Lucks

1
@VictorMolina Hayır, bu kesinlikle abartılı olur. Kararlı olmayan, yani eşzamansız sonuç dönmeden önce bağlantısı kesilebilen ve durum olarak ayarlanmaya hazır olan a) fetchin useEffectve b) gibi eşzamansız işlemleri kullanan bileşenler için bu tekniği düşünün .
ford04

83

Kaldırmak için - Bağlanmayan bir bileşen uyarısında React durum güncellemesi gerçekleştirilemez, bir koşul altında componentDidMount yöntemini kullanın ve componentWillUnmount yönteminde bu koşulu yanlış yapın. Örneğin : -

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

2
Bu işe yaradı, ama bu neden işe yarasın? Bu hataya tam olarak ne sebep olur? ve bunun nasıl düzeltildiğini: |
Abhinav

İyi çalışıyor. SetState çağrısından önce _isMounted değerini doğruladığı için setState yönteminin tekrarlayan çağrısını durdurur, ardından en sonunda componentWillUnmount () içinde tekrar false olarak sıfırlanır. Sanırım bu böyle çalışıyor.
Abhishek

7
kanca bileşeni için şunu kullanın:const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
x-magix

@ x-magix Bunun için gerçekten bir ref'e ihtiyacınız yok, sadece return fonksiyonunun kapatabileceği bir yerel değişken kullanabilirsiniz.
Mordechai

@Abhinav Bunun neden işe yaradığını tahmin ettiğim en iyi tahmin, bunun _isMountedReact tarafından yönetilmemesi (aksine state) ve bu nedenle React'in işleme hattına tabi olmaması . Buradaki sorun, bir bileşenin bağlantısı kaldırılmak üzere ayarlandığında, React'in herhangi bir çağrıyı kuyruğundan çıkarmasıdır setState()(bu, bir 'yeniden işleme'yi tetikler); bu nedenle, durum hiçbir zaman güncellenmez
Lightfire228

26

Yukarıdaki çözümler işe yaramazsa, bunu deneyin ve benim için çalışıyor:

componentWillUnmount() {
    // fix Warning: Can't perform a React state update on an unmounted component
    this.setState = (state,callback)=>{
        return;
    };
}

Teşekkürler benim için çalışıyor. Biri bana bu kod parçasını açıklayabilir mi?
Badri Paudel

@BadriPaudel, çıkış bileşeni olduğunda boş döndürür, artık bellekte herhangi bir veri tutmayacaktır
Mayıs Hava Durumu VN

Bunun için çok teşekkür ederim!
Tushar Gupta

ne dönecek? olduğu gibi yapıştırmak mı?
artı


5

değiştirmeyi deneyin setDivSizeThrottleableiçin

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

Ben denedim. Şimdi, bu değişikliği yapmadan önce pencereyi yeniden boyutlandırırken yalnızca zaman zaman gözlemlediğim uyarıyı sürekli olarak görüyorum. ¯_ (ツ) _ / ¯ Yine de bunu denediğiniz için teşekkürler.
Igor Soloydenko

3

Geçmişi kullanmadığınızı biliyorum, ancak benim durumumda useHistory, React Bağlam Sağlayıcımda durum kalıcı olmadan önce bileşenin bağlantısını kesen React Router DOM'un kancasını kullanıyordum .

Bu sorunu çözmek withRouteriçin, benim durumumda export default withRouter(Login)ve bileşenin içinde bileşeni yerleştiren kancayı kullandım const Login = props => { ...; props.history.push("/dashboard"); .... Diğerini de props.history.pushbileşenden kaldırdım , örneğin if(authorization.token) return props.history.push('/dashboard')bu durum nedeniyle bir döngüye neden olduğu için authorization.

Bir alternatif yeni bir öğe itmek tarih .


2

Axios'tan veri alıyorsanız ve hata hala devam ediyorsa, ayarlayıcıyı koşulun içine sarmanız yeterlidir.

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

1

Düzenleme: Uyarının adlı bir bileşene başvurduğunu fark ettim TextLayerInternal. Muhtemelen böceğiniz burada. Bunun geri kalanı hala geçerlidir, ancak sorununuzu çözmeyebilir.

1) Bu uyarı için bir bileşenin örneğini almak zordur. React'te bunu geliştirmek için bazı tartışmalar var gibi görünüyor ancak şu anda bunu yapmanın kolay bir yolu yok. Henüz oluşturulmamış olmasının nedeni, muhtemelen bileşenlerin, unmount'tan sonra setState'in bileşenin durumu ne olursa olsun mümkün olmayacağı şekilde yazılmasının beklenmesidir. React ekibi söz konusu olduğunda sorun her zaman Bileşen kodundadır ve Bileşen örneğinde değildir, bu nedenle Bileşen Türü adını alırsınız.

Bu cevap tatmin edici olmayabilir, ancak sorununuzu çözebileceğimi düşünüyorum.

2) Lodashes throttled fonksiyonu bir cancelmetoda sahiptir. Çağrı canceliçinde componentWillUnmountve hendek isComponentMounted. İptal etmek, yeni bir özellik sunmaktan daha "deyimsel olarak" tepki vermektir.


Sorun şu ki, doğrudan kontrol etmiyorum TextLayerInternal. Bu nedenle, "kimin suçu setState()arama" bilmiyorum . cancelTavsiyenize göre deneyeceğim ve nasıl gittiğini göreceğim
Igor Soloydenko

Ne yazık ki hala uyarıyı görüyorum. İşleri doğru şekilde yaptığımı doğrulamak için lütfen Güncelleme 1 bölümündeki kodu kontrol edin.
Igor Soloydenko

1

Benzer bir sorun yaşadım teşekkürler @ ford04 bana yardımcı oldu.

Ancak başka bir hata oluştu.

NB. ReactJS kancaları kullanıyorum

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.

Hataya ne sebep olur?

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

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

Bu işe yaramadı çünkü yeni sayfaya yönlendiriyor olmanıza rağmen tüm durum ve proplar dom üzerinde manipüle ediliyor ya da sadece önceki sayfaya işleme durmadı.

Hangi çözümü buldum

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

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

0

Benzer bir sorun yaşadım ve çözdüm:

Redux ile ilgili bir eylem göndererek kullanıcının otomatik olarak oturum açmasını sağlıyordum (doğrulama jetonunu redux durumuna yerleştirerek)

ve sonra bileşenimde this.setState ({succ_message: "...") ile bir mesaj göstermeye çalışıyordum.

Bileşen, konsolda aynı hatayla boş görünüyordu: "çıkarılmış bileşen" .. "bellek sızıntısı" vb.

Walter'ın bu konudaki cevabını okuduktan sonra

Uygulamamın Yönlendirme tablosunda, kullanıcı oturum açmışsa bileşenimin rotasının geçerli olmadığını fark ettim:

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

Jeton var olsa da olmasa da Rotayı görünür hale getirdim.


0

@ Ford04 cevabına göre, burada aynı yöntem bir yöntemde özetlenmiştir:

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

Kullanım:

const MyComponent : FC<{}> = (props) => {
    const [ asyncProp , setAsyncProp ] = useState( '' ) ;
    useEffectAsync( async ( isMounted ) =>
    {
        const someAsyncProp = await ... ;
        if ( isMounted() )
             setAsyncProp( someAsyncProp ) ;
    });
    return <div> ... ;
} ;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.