Не вдається виконати оновлення стану React для немонтованого компонента


143

Проблема

Я пишу заяву в React і не змогли уникнути супер загальної пастки, який дзвонить 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

1
@ ic3b3rg Все те саме попередження про час роботи.
Ігор Солойденко

Відповіді:


95

Ось конкретне рішення для React Hooks

Помилка

Попередження: Не вдається виконати оновлення стану React для немонтованого компонента.

Рішення

Ви можете заявити 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гачок

Ми можемо інкапсулювати весь шаблон в спеціальний Хук, який просто знає, як поводитись і автоматично скасовувати функції асинхронізації на випадок, якщо компонент демонтується раніше:

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


2
твої трюки працюють! Цікаво, в чому криється магія?
Niyongabo

1
Тут ми використовуємо вбудовану функцію очищення ефекту , яка працює, коли залежності змінюються, і в будь-якому випадку, коли компонент демонтується. Отже, це ідеальне місце для перемикання isMountedпрапора, до falseякого можна отримати доступ із зони закриття зворотного виклику ефекту. Ви можете думати про функцію очищення як про належність до її відповідного ефекту.
ford04 02

1
це має сенс! Я рада вашій відповіді. Я навчився з цього.
Niyongabo

1
@VictorMolina Ні, це, безумовно, було б надмірним. Розглянемо цей прийом для компонентів а) з використанням асинхронних операцій, таких як fetchin useEffectі b), які не є стабільними, тобто можуть бути демонтовані до повернення результату асинхронізації та готові до встановлення як стану.
ford04

1
stackoverflow.com/a/63213676 і medium.com/better-programming / ... були цікаві , але в кінцевому рахунку , ваш відповідь , що , нарешті , допомогли мені отримати мій працювати. Дякую!
Райан

87

Щоб видалити - Не вдається виконати оновлення стану React для попередженого попередження про компонент, використовуйте метод componentDidMount під умовою та зробіть помилковим цю умову для методу componentWillUnmount. Наприклад : -

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, оскільки перевіряє значення _isMount перед викликом setState, а потім нарешті знову скидає значення false у компонентіWillUnmount (). Думаю, це так працює.
Абхішек

8
для хук-компонента використовуйте це:const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
x-magix

@ x-magix Для цього вам насправді не потрібно посилання, ви можете просто використовувати локальну змінну, яку функція повернення може закрити.
Мордехай

@Abhinav Я найкраще здогадуюсь, чому це працює - це те, що _isMountedне управляється React (на відміну від state), і тому не підлягає конвеєру рендерингу React . Проблема полягає в тому, що коли компонент налаштовано на демонтаж, React дезактивує будь-які виклики setState()(що спричинить повторне відображення); тому держава ніколи не оновлюється
Lightfire228

35

Якщо наведені вище рішення не працюють, спробуйте це, і це працює для мене:

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

1
Дякую, це працює для мене. Хто-небудь може пояснити мені цей шматок коду?
Бадрі Паудель

@BadriPaudel повертає значення null під час виходу компонента, він більше не буде зберігати дані в пам'яті
May'Habit

Щиро дякую за це!
Тушар Гупта,

повернути що? просто вставте так, як є?
плюс


5

спробуйте змінити setDivSizeThrottleableна

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

Я спробував. Зараз я постійно бачу попередження, яке я лише час від часу спостерігав за зміною розміру вікна, перш ніж вносити зміни. ¯_ (ツ) _ / ¯ Хоча дякую, що спробували.
Ігор Солойденко

5

Я знаю, що ви не використовуєте історію, але в моєму випадку я використовував useHistoryхук від React Router DOM, який демонтує компонент до того, як стан зберігається у моєму постачальнику React Context.

Щоб вирішити цю проблему, я використав гачок, що 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

Якщо ви отримуєте дані з аксіо, але помилка все-таки виникає, просто оберніть установку всередині умови

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) Отримати екземпляр компонента для цього попередження важко. Схоже, є певна дискусія щодо покращення цього в React, але в даний час не існує простого способу це зробити. Причина, по якій він ще не побудований, я підозрюю, швидше за все тому, що компоненти, як очікується, будуть записані таким чином, що setState після демонтажу неможливо, незалежно від стану компонента. Що стосується команди React, проблема полягає завжди в коді Component, а не в екземплярі Component, саме тому ви отримуєте ім'я Component Type.

Ця відповідь може бути незадовільною, але я думаю, що я можу вирішити вашу проблему.

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/>повернення до сторінки, яка вже була змонтована у віртуальному DOM, тому вимагається отримання даних із життєвого циклу componentDidMount


Ви хочете сказати, що componentDidMount()можна було componentWillUnmount()дзвонити двічі без проміжного дзвінка між ними? Я не думаю, що це можливо.
Alexis Wilke

1
Ні, я кажу, що він не викликається двічі, саме тому сторінка не обробляє код всередині componentDidMount()при використанні <Link/>. Я використовую Redux для вирішення цих проблем і зберігаю дані веб-сторінки в сховищі редуктора, так що мені все одно не потрібно перезавантажувати сторінку.
coder9833idls

0

У мене була подібна проблема і я її вирішив:

Я автоматично робив користувача зареєстрованим, надсилаючи дію на redux (розміщуючи маркер автентифікації на стан 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> ... ;
} ;

0

Натхненний прийнятою відповіддю @ ford04, я мав би навіть кращий підхід до вирішення цього питання, замість того, щоб використовувати useEffectвсередині, useAsyncстворити нову функцію, яка повертає зворотний виклик для 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)
    },[])


Я б не рекомендував покладатися на локальну isMountedзмінну, але замість цього зробіть її станом (через useStateхук).
Ігор Солойденко

Як це має значення? По крайней мере, я не можу подумати про іншу поведінку.
guneetgstar
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.