एक अनमाउंट घटक पर एक प्रतिक्रिया स्थिति अद्यतन नहीं कर सकता


142

मुसीबत

मैं रिएक्ट में एक आवेदन लिख रहा हूं और एक सुपर कॉमन पिटफॉल से बचने में असमर्थ था, जो setState(...)बाद में बुला रहा है componentWillUnmount(...)

मैंने अपने कोड को बहुत ध्यान से देखा और कुछ गार्डिंग क्लॉस लगाने की कोशिश की, लेकिन समस्या बनी रही और मैं अभी भी चेतावनी का पालन कर रहा हूं।

इसलिए, मुझे दो प्रश्न मिले हैं:

  1. मैं स्टैक ट्रेस से कैसे पता लगाऊं कि कौन सा विशेष घटक और घटना हैंडलर या जीवनचक्र हुक नियम के उल्लंघन के लिए जिम्मेदार है?
  2. ठीक है, समस्या को स्वयं कैसे ठीक किया जाए, क्योंकि मेरा कोड इस गड्ढे को ध्यान में रखकर लिखा गया था और पहले से ही इसे रोकने की कोशिश कर रहा है, लेकिन कुछ अंतर्निहित घटक अभी भी चेतावनी पैदा कर रहे हैं।

ब्राउज़र कंसोल

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

यहाँ छवि विवरण दर्ज करें

कोड

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

अद्यतन 1: रद्द थ्रोटेबल फ़ंक्शन (अभी भी भाग्य नहीं)

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;

क्या समस्या बनी रहती है यदि आप टिप्पणी जोड़ते हैं और श्रोताओं को हटाते हैं?
ic3b3rg

@ ic3b3rg समस्या गायब हो जाती है अगर कोई घटना सुनने का कोड नहीं है
इगोर सोलोय्डेनको

ठीक है, क्या आपने सुझाव को गार्ड के this.setDivSizeThrottleable.cancel()बजाय करने की कोशिश की this.isComponentMounted?
ic3b3rg 20

1
@ ic3b3rg फिर भी वही रन-टाइम चेतावनी।
इगोर सोलोय्डेनको

जवाबों:


94

यहाँ के लिए एक प्रतिक्रिया हुक विशिष्ट समाधान है

त्रुटि

चेतावनी: अनमाउंट किए गए घटक पर प्रतिक्रिया स्थिति अद्यतन नहीं कर सकता।

उपाय

आप let isMounted = trueअंदर की घोषणा कर सकते हैं useEffect, जो घटक के अनमाउंट होने के साथ ही क्लीनअप कॉलबैक में बदल जाएगी। राज्य अपडेट से पहले, अब आप इस चर को सशर्त रूप से जांचते हैं:

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

एक्सटेंशन: कस्टम useAsyncहुक

हम सभी बॉयलरप्लेट को एक कस्टम हुक में इनकैप्सुलेट कर सकते हैं, बस यह जानते हैं कि घटक से पहले अनमाउंट होने की स्थिति में async फ़ंक्शन को स्वचालित रूप से कैसे निपटाया जाता है।

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


2
आपकी चाल काम करती है! मुझे आश्चर्य है कि जादू के पीछे क्या है?
निओगाबो

1
हम यहां अंतर्निहित प्रभाव सफाई सुविधा का लाभ उठाते हैं, जो निर्भरता बदलने पर और घटक के अनमाउंट होने पर या तो मामले में चलता है। तो यह एक isMountedध्वज को टॉगल करने के लिए सही जगह है false, जिसे आसपास के प्रभाव कॉलबैक क्लोजर दायरे से एक्सेस किया जा सकता है। आप के रूप में सफाई समारोह के बारे में सोच सकते हैं से संबंधित अपनी इसी प्रभाव।
ford04

1
यह समझ में आता है! मैं आपके जवाब से खुश हूं। मैंने इससे सीखा।
नियांगबो

1
@VictorMolina नहीं, यह निश्चित रूप से ओवरकिल होगा। घटकों एक) की तरह अतुल्यकालिक संचालन प्रयोग करने के लिए इस तकनीक पर विचार fetchमें useEffectऔर ख) कि स्थिर नहीं कर रहे हैं, यानी async परिणाम रिटर्न से पहले अनमाउंट कर दिया हो सकता है और राज्य के रूप में स्थापित होने के लिए तैयार है।
ford04

1
stackoverflow.com/a/63213676 और medium.com/better-programming/… दिलचस्प थे लेकिन अंततः आपका जवाब है कि आखिरकार मुझे मेरा काम करने में मदद मिली। धन्यवाद!
रयान

87

हटाने के लिए - एक अनमाउंट घटक चेतावनी पर एक रिएक्ट स्टेट अपडेट नहीं कर सकते हैं, किसी शर्त के तहत कंपोनेंटडिमाउंट विधि का उपयोग करें और कंपोनेंटविलाउमाउंट विधि पर उस स्थिति को गलत बनाते हैं। उदाहरण के लिए : -

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
यह काम किया, लेकिन यह काम क्यों करना चाहिए? क्या वास्तव में इस त्रुटि का कारण बनता है? और यह कैसे तय किया: |
अभिनव

यह ठीक काम करता है। यह setState पद्धति की दोहरावदार कॉल को रोकता है क्योंकि यह setState कॉल से पहले _isMounted मान को मान्य करता है, फिर अंतिम रूप से फिर से फिर से portWillUnmount () में झूठे को रीसेट करता है। मुझे लगता है, यही तरीका काम करता है।
अभिषेक

8
हुक कंपोनेंट के लिए इसका उपयोग करें:const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
x-magix

@ x-magix आपको इसके लिए वास्तव में रेफरी की आवश्यकता नहीं है, आप बस एक स्थानीय वैरिएबल का उपयोग कर सकते हैं जिस पर रिटर्न फंक्शन बंद हो सकता है।
मोर्दकै

@ अभिनव मेरा सबसे अच्छा अनुमान है कि यह क्यों काम करता है जो कि _isMountedरिएक्ट (विपरीत state) द्वारा प्रबंधित नहीं है और इसलिए रिएक्ट के प्रतिपादन पाइपलाइन के अधीन नहीं है । मुद्दा यह है कि जब एक घटक को अनमाउंट करने के लिए सेट किया जाता है, तो प्रतिक्रिया किसी भी कॉल को रोकती है setState()(जो एक 'पुनः-रेंडर' को ट्रिगर करेगा); इसलिए, राज्य को कभी अद्यतन नहीं किया जाता है
लाइटफायर 228 22:15

35

यदि उपरोक्त समाधान काम नहीं करते हैं, तो यह कोशिश करें और यह मेरे लिए काम करता है:

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

1
धन्यवाद यह मेरे लिए काम करता है। किसी ने मुझे इस कोड का टुकड़ा समझा सकते हैं?
बद्री पौडेल

@BadriPaudel एस्कैप कंपोनेंट के समय वापस आ जाता है, यह मेमोरी में कोई डेटा नहीं रखेगा
May'Habit

इस के लिए बहुत बहुत धन्यवाद!
तुषार गुप्ता

क्या लौटा? बस इसे वैसे ही चिपकाएँ?
प्लस

11

संभवतः setStateप्रभाव हुक से कॉल करने के कारण मुझे यह चेतावनी मिली थी (यह एक साथ जुड़े इन 3 मुद्दों में चर्चा की गई है )।

वैसे भी, प्रतिक्रिया संस्करण के उन्नयन ने चेतावनी को हटा दिया।


5

को बदलने setDivSizeThrottleableका प्रयास करें

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

मैंने इसे आजमाया। अब मैं लगातार उस चेतावनी को देख रहा हूं जिसे मैं केवल इस बदलाव से पहले खिड़की के आकार बदलने पर समय-समय पर देख रहा था। ¯_ (¯) _ /। हालांकि यह कोशिश करने के लिए धन्यवाद।
इगोर सोलोय्डेनको

5

मुझे पता है कि आप इतिहास का उपयोग नहीं कर रहे हैं, लेकिन मेरे मामले में मैं useHistoryरिएक्ट राउटर डीओएम से हुक का उपयोग कर रहा था , जो कि मेरे रिएक्ट कॉन्टेक्स्ट प्रदाता में राज्य के बने रहने से पहले घटक को अनमाउंट करता है।

इस समस्या को ठीक करने के लिए मैंने हुक का उपयोग किया withRouterहै घटक, मेरे मामले में export default withRouter(Login), और घटक के अंदर const Login = props => { ...; props.history.push("/dashboard"); ...। मैंने props.history.pushघटक से अन्य को भी हटा दिया है , उदाहरण के लिए, if(authorization.token) return props.history.push('/dashboard')क्योंकि यह एक लूप का कारण बनता है, क्योंकि authorizationराज्य।

इतिहास के लिए एक नई वस्तु को आगे बढ़ाने का विकल्प ।


2

यदि आप axios से डेटा ला रहे हैं और त्रुटि अभी भी होती है, तो बस कंडक्टर को शर्त के अंदर लपेटें

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

एक हुक है जो काफी सामान्य कहा जाता है useIsMountedजो इस समस्या को हल करता है (कार्यात्मक घटकों के लिए) ...

import { useRef, useEffect } from 'react';

export function useIsMounted() {
  const isMounted = useRef(false);

  useEffect(() => {
    isMounted.current = true;
    return () => isMounted.current = false;
  }, []);

  return isMounted;
}

फिर अपने कार्यात्मक घटक में

function Book() {
  const isMounted = useIsMounted();
  ...

  useEffect(() => {
    asyncOperation().then(data => {
      if (isMounted.current) { setState(data); }
    })
  });
  ...
}

1

संपादित करें: मुझे बस एहसास हुआ कि चेतावनी नामक एक घटक को संदर्भित कर रहा है TextLayerInternal। यह संभावना है कि आपका बग कहां है। इसका बाकी हिस्सा अभी भी प्रासंगिक है, लेकिन यह आपकी समस्या को ठीक नहीं कर सकता है।

1) इस चेतावनी के लिए एक घटक का उदाहरण प्राप्त करना कठिन है। ऐसा लगता है कि रिएक्ट में इसे सुधारने के लिए कुछ चर्चा की गई है लेकिन वर्तमान में इसे करने का कोई आसान तरीका नहीं है। इसका कारण यह अभी तक नहीं बनाया गया है, मुझे संदेह है, संभावना है क्योंकि घटकों को इस तरह से लिखे जाने की उम्मीद है कि अनमाउंट के बाद सेटस्ट्रेट संभव नहीं है, कोई फर्क नहीं पड़ता कि घटक की स्थिति क्या है। जहां तक ​​रिएक्ट टीम की बात है, समस्या हमेशा कंपोनेंट कोड में होती है, न कि कंपोनेंट उदाहरण में, जिसके कारण आपको कंपोनेंट टाइप नाम मिलता है।

यह उत्तर असंतोषजनक हो सकता है, लेकिन मुझे लगता है कि मैं आपकी समस्या को ठीक कर सकता हूं।

2) लॉडैश थ्रोटल्ड फ़ंक्शन में एक cancelविधि है। कॉल cancelमें componentWillUnmountऔर खाई isComponentMounted। रद्द करना एक नई संपत्ति शुरू करने की तुलना में अधिक "मुहावरेदार" प्रतिक्रिया है।


मुद्दा है, मैं सीधे नियंत्रण नहीं है TextLayerInternal। इस प्रकार, मुझे नहीं पता "कौन गलती है setState()कॉल"। मैं cancelआपकी सलाह के अनुसार प्रयास करूँगा और देखूँगा कि यह कैसे जाता है,
इगोर सोलोय्डेनको

दुर्भाग्य से, मैं अभी भी चेतावनी देख रहा हूं। कृपया सत्यापित करें कि मैं चीजों को सही तरीके से करने के लिए अपडेट 1 अनुभाग में कोड की जांच कर रहा हूं।
इगोर सोलोय्डेनको

1

मेरे पास एक समान मुद्दा था धन्यवाद @ ford04 ने मेरी मदद की।

हालाँकि, एक और त्रुटि हुई।

एनबी। मैं 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 />
  </>
)

यह काम नहीं कर सका क्योंकि आप नए पेज पर रीडायरेक्ट होने के बावजूद सभी स्टेट और प्रॉप्स को डोम पर मैनिपुलेट किया जा रहा है या पिछले पेज पर रेंडर करने से रोक नहीं लगी है।

मुझे क्या हल मिला

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

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

1

आप अपने वेबपेज को कैसे खोलते हैं, इसके आधार पर, आप बढ़ते का कारण नहीं बन सकते हैं। जैसे <Link/>कि पहले से ही वर्चुअल डोम में लगे पेज पर वापस जाने के लिए, इसलिए किसी घटकडीडमाउंट जीवनचक्र के डेटा की आवश्यकता होती है।


क्या आप कह रहे हैं कि बीच में componentDidMount()बिना बुलाए दो बार componentWillUnmount()कॉल किया जा सकता है ? मुझे नहीं लगता कि यह संभव है।
एलेक्सिस विलके

1
नहीं, मैं कह रहा हूं कि इसे दो बार नहीं कहा जाता है, यही कारण है कि पेज componentDidMount()का उपयोग करते समय कोड को अंदर संसाधित नहीं करता है <Link/>। मैं इन समस्याओं के लिए Redux का उपयोग करता हूं और वेब पेज के डेटा को Reducer स्टोर में रखता हूं ताकि मुझे पेज को फिर से लोड करने की आवश्यकता न हो।
coder9833idls

0

मुझे एक समान समस्या थी और इसे हल किया:

मैं स्वतः ही उपयोगकर्ता को लॉग-इन (redux राज्य पर प्रमाणीकरण टोकन भेजकर) पर एक कार्रवाई भेजकर लॉग-इन कर रहा था

और फिर मैं अपने कंपोनेंट में this.setState ({succ_message: "...") के साथ एक संदेश दिखाने की कोशिश कर रहा था।

कंसोल पर एक ही त्रुटि के साथ घटक खाली दिख रहा था: "अनमाउंट किया गया घटक" .. "मेमोरी लीक" आदि।

बाद मैंने वाल्टर के जवाब को इस सूत्र में पढ़ा

मैंने देखा है कि मेरे आवेदन के रूटिंग टेबल में, उपयोगकर्ता द्वारा लॉग-इन किए जाने पर मेरे घटक का मार्ग मान्य नहीं था:

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

मैंने रूट को दिखाई दिया कि टोकन मौजूद है या नहीं।


0

@ Ford04 उत्तर के आधार पर, यहाँ एक विधि में एक ही समझाया गया है:

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

उपयोग:

const MyComponent : FC<{}> = (props) => {
    const [ asyncProp , setAsyncProp ] = useState( '' ) ;
    useEffectAsync( async ( isMounted ) =>
    {
        const someAsyncProp = await ... ;
        if ( isMounted() )
             setAsyncProp( someAsyncProp ) ;
    });
    return <div> ... ;
} ;
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.