Невозможно выполнить обновление состояния React на отключенном компоненте

Проблема

Я пишу приложение на 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

Невозможно выполнить обновление состояния React на отключенном компоненте

Код

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 27.12.2018 20:20

@ ic3b3rg проблема исчезает, если нет кода прослушивания событий

Igor Soloydenko 27.12.2018 21:26

ок, а предложение сделать this.setDivSizeThrottleable.cancel() вместо гвардии this.isComponentMounted пробовали?

ic3b3rg 27.12.2018 21:30

@ ic3b3rg То же предупреждение во время выполнения.

Igor Soloydenko 28.12.2018 00:38

Возможный дубликат React - setState () на размонтированном компоненте

Emile Bergeron 27.08.2019 21:23
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
309
5
443 748
27
Перейти к ответу Данный вопрос помечен как решенный

Ответы 27

Обновлено: я только что понял, что предупреждение относится к компоненту TextLayerInternal. Вероятно, это ваша ошибка. Остальное по-прежнему актуально, но, возможно, это не решит вашу проблему.

1) Получить экземпляр компонента для этого предупреждения сложно. Похоже, что есть некоторые обсуждения по улучшению этого в React, но в настоящее время нет простого способа сделать это. Я подозреваю, что причина, по которой он еще не был построен, вероятно, заключается в том, что компоненты должны быть написаны таким образом, что setState после размонтирования невозможно, независимо от состояния компонента. Проблема, с точки зрения команды React, всегда заключается в коде компонента, а не в экземпляре компонента, поэтому вы получаете имя типа компонента.

Этот ответ может быть неудовлетворительным, но я думаю, что смогу решить вашу проблему.

2) Регулируемая функция Lodashes имеет метод cancel. Вызовите cancel в componentWillUnmount и откажитесь от isComponentMounted. Отмена - это более «идиоматическая» реакция, чем введение нового свойства.

Проблема в том, что я не контролирую TextLayerInternal напрямую. Таким образом, я не знаю, «кто виноват в звонке setState()». Я попробую cancel по вашему совету и посмотрю, как он пойдет,

Igor Soloydenko 27.12.2018 21:29

К сожалению, я все еще вижу предупреждение. Пожалуйста, проверьте код в разделе «Обновление 1», чтобы убедиться, что я все делаю правильно.

Igor Soloydenko 28.12.2018 00:37

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

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

Я пробовал. Теперь я постоянно вижу предупреждение, которое я наблюдал только время от времени, при изменении размера окна перед внесением этого изменения. ¯_ (ツ) _ / ¯ Тем не менее, спасибо за попытку.

Igor Soloydenko 28.12.2018 02:09

To remove - Can't perform a React state update on an unmounted component warning, use componentDidMount method under a condition and make false that condition on componentWillUnmount method. For example : -

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

Это сработало, но почему это должно работать? Что именно вызывает эту ошибку? и как это исправить: |

Abhinav 22.02.2020 20:55

Работает нормально. Он останавливает повторяющийся вызов метода setState, поскольку он проверяет значение _isMounted перед вызовом setState, а затем, наконец, снова сбрасывает значение false в componentWillUnmount (). Я думаю, так оно и работает.

Abhishek 02.03.2020 11:51

для компонента крючка используйте это: const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });

x-magix 22.05.2020 15:37

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

Mordechai 15.06.2020 23:25

@Abhinav Я думаю, почему это работает, так это то, что _isMounted не управляется React (в отличие от state) и, следовательно, не подчиняется конвейер рендеринга React. Проблема в том, что когда компонент настроен на размонтирование, React удаляет из очереди все вызовы setState() (что может вызвать «повторную визуализацию»); поэтому состояние никогда не обновляется

Lightfire228 16.10.2020 00:35

Если вы используете Babel и включили экспериментальные предложения JavaScript, вы можете использовать поле частного класса для флага, то есть this.#isMounted

Stefan Becker 27.01.2021 10:16

Фантастика! У меня сработало нормально.

Joseph 04.04.2021 14:29

@Mordechai Я считаю, что это было сделано, чтобы избежать следующего предупреждения компилятора: `` Назначения переменной isMounted изнутри React Hook useEffect будут потеряны после каждого рендеринга. Чтобы сохранить значение с течением времени, сохраните его в хуке useRef и сохраните изменяемое значение в свойстве .current. В противном случае вы можете переместить эту переменную прямо внутрь useEffect `` ''

havish 01.06.2021 08:50

Вы не получите это предупреждение о том, что переменная объявлена ​​локально в useEffect и пустом массиве зависимостей.

Mordechai 01.06.2021 15:13

простое для понимания решение является большим преимуществом по сравнению со сложным для понимания js в некоторых других решениях, плюс ответ показан в контексте компонента. отличный ответ.

Will 30.09.2021 17:11

У меня было это предупреждение, возможно, из-за вызова setState из обработчика эффектов (это обсуждается в этих 3 вопросысвязанывместе).

Во всяком случае, обновление версии реакции удалило предупреждение.

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

Чтобы решить эту проблему, я использовал ловушку withRouter, встраивающую компонент, в моем случае export default withRouter(Login), и внутри компонента const Login = props => { ...; props.history.push("/dashboard"); .... Я также удалил другой props.history.push из компонента, например if (authorization.token) return props.history.push('/dashboard'), потому что это вызывает цикл из-за состояния authorization.

Альтернатива помещению нового элемента в история.

Ответ принят как подходящий

Вот конкретное решение Реагировать на хуки для

Ошибка

Warning: Can't perform a React state update on an unmounted component.

Решение

Вы можете объявить let isMounted = true внутри useEffect, который будет изменен в обратный вызов очистки, как только компонент будет размонтирован. Перед обновлением состояния вы теперь проверяете эту переменную условно:

useEffect(() => {
  let isMounted = true;               // note mutable flag
  someAsyncOperation().then(data => {
    if (isMounted) setState(data);    // add conditional check
  })
  return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);                               // adjust dependencies to your needs

const Parent = () => {
  const [mounted, setMounted] = useState(true);
  return (
    <div>
      Parent:
      <button onClick = {() => setMounted(!mounted)}>
        {mounted ? "Unmount" : "Mount"} Child
      </button>
      {mounted && <Child />}
      <p>
        Unmount Child, while it is still loading. It won't set state later on,
        so no error is triggered.
      </p>
    </div>
  );
};

const Child = () => {
  const [state, setState] = useState("loading (4 sec)...");
  useEffect(() => {
    let isMounted = true;
    fetchData();
    return () => {
      isMounted = false;
    };

    // simulate some Web API fetching
    function fetchData() {
      setTimeout(() => {
        // drop "if (isMounted)" to trigger error again 
        // (take IDE, doesn't work with stack snippet)
        if (isMounted) setState("data fetched")
        else console.info("aborted setState on unmounted component")
      }, 4000);
    }
  }, []);

  return <div>Child: {state}</div>;
};

ReactDOM.render(<Parent />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

Расширение: Custom useAsync Hook

Мы можем инкапсулировать весь шаблон в настраиваемый Hook, который автоматически прерывает асинхронные функции в случае, если компонент отключен или значения зависимостей изменились ранее:

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

// custom Hook for automatic abortion on unmount or dependency change
// You might add onFailure for promise errors as well.
function useAsync(asyncFn, onSuccess) {
  useEffect(() => {
    let isActive = true;
    asyncFn().then(data => {
      if (isActive) onSuccess(data)
      else console.info("aborted setState on unmounted component")
    });
    return () => {
      isActive = false;
    };
  }, [asyncFn, onSuccess]);
}

const Child = () => {
  const [state, setState] = useState("loading (4 sec)...");
  useAsync(simulateFetchData, setState);
  return <div>Child: {state}</div>;
};

const Parent = () => {
  const [mounted, setMounted] = useState(true);
  return (
    <div>
      Parent:
      <button onClick = {() => setMounted(!mounted)}>
        {mounted ? "Unmount" : "Mount"} Child
      </button>
      {mounted && <Child />}
      <p>
        Unmount Child, while it is still loading. It won't set state later on,
        so no error is triggered.
      </p>
    </div>
  );
};

const simulateFetchData = () => new Promise(
  resolve => setTimeout(() => resolve("data fetched"), 4000));

ReactDOM.render(<Parent />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

Подробнее об очистке эффектов: Чрезмерная реакция: полное руководство по использованию эффекта

твои уловки работают! Интересно, в чем заключается магия?

Niyongabo Eric 02.07.2020 21:38

Здесь мы используем встроенный эффект уборка, который запускается при изменении зависимостей и в любом случае при отключении компонента. Так что это идеальное место для переключения флага isMounted на false, доступ к которому можно получить из области закрытия обратного вызова окружающего эффекта. Вы можете думать о функции очистки как о принадлежащий к ее соответствующем эффекте.

ford04 03.07.2020 01:28

это имеет смысл! Я доволен твоим ответом. Я извлек из этого урок.

Niyongabo Eric 03.07.2020 05:38

святой курит ... эта штука isMounted работает. Я использую response-testing-lib 10.4.7, а на formik ^2.1.4. Это похоже на полный взлом и результат чего-то с Formik.

Phil Lucks 17.07.2020 17:27

@PhilLucks рад, что это работает. Этот шаблон - очень распространенный и простой способ прервать асинхронные функции, без взлома (если я не ошибаюсь, аналогичный подход даже рекомендуется в FAQ по хукам).

ford04 17.07.2020 17:48

Итак, должны ли мы обернуть все наши обновления состояния в if (isMounted) {/ * здесь обновление состояния * /} ??

Victor Molina 04.11.2020 07:51

@VictorMolina Нет, это определенно было бы излишним. Рассмотрим этот метод для компонентов a), использующих асинхронные операции, такие как fetch в useEffect и b), которые нестабильны, т.е. могут быть размонтированы до того, как будет возвращен результат async, и готовы к установке в качестве состояния.

ford04 04.11.2020 07:59

Ой! Добавив await в мои функции async, он работает так, как ожидалось!

Alexis Wilke 24.12.2020 19:34

@AlexisWilke: да, вы можете определить внутреннюю функцию async внутри тела обратного вызова useEffectкак показано в этом посте. Таким образом, вы можете использовать await вместо myPromise.then(...).

ford04 24.12.2020 23:14
stackoverflow.com/a/63213676 и medium.com/better-programming/… были интересны, но в конечном итоге ваш ответ - это то, что наконец помогло мне заставить мою работу. Спасибо!
Ryan 07.01.2021 00:30

@Goran_Ilic_Ilke, мы этого не делаем. return () => { isMounted = false }; означает: вернуть нулевой параметр стрелочная функция, тип возврата которого - void, поскольку внутри мы применяем побочный эффект переключения переменной состояния isMounted. Я думаю, вы перепутали его с return () => ({ isMounted = false });, см. Ссылку выше.

ford04 21.02.2021 11:25

Это кажется хорошим решением, если вы уже сузили круг причин / useEffect, вызывающих проблему, но в одном из вопросов 1. спрашивается, как определить, какой компонент, обработчик, перехватчик и т. д. Несет ответственность за эту ошибку. Почему так много положительных отзывов о проблеме, в которых не упоминается вся проблема в целом и не даются ответы на оба вопроса? Я проверил, и во всей этой ветке также нет сообщений о каких-либо советах по поиску источника ошибки; по трассировке стека невозможно выяснить?

Jonathan 18.06.2021 06:25

Обратите внимание, что пользовательский хук useAsync будет срабатывать при каждом рендеринге, если вы не оберните обратные вызовы в useCallback.

Woodz 02.07.2021 14:30

@Woodz да, хороший намек. useCallback - это обычный и рекомендуемый способ в React возложить ответственность за зависимости на клиента useAsync. Вы можете переключиться на изменяемые ссылки внутри useAsync для хранения самого последнего обратного вызова, чтобы клиенты могли напрямую передавать свои функции / обратные вызовы без зависимостей. Но я бы экономно использовал этот шаблон, поскольку, вероятно, это более запутанный и императивный подход.

ford04 03.07.2021 11:55

Вопрос, если я просто проигнорирую это, может ли это повредить моему приложению? Я пока не видел, чтобы это повредило или испортило мое приложение.

ReactPotato 17.11.2021 07:40

Если вышеуказанные решения не работают, попробуйте это, и это сработает для меня:

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

@BadriPaudel возвращает null при экранировании компонента, он больше не будет хранить какие-либо данные в памяти

May'Habit 23.06.2020 16:49

что вернуть? просто наклеить как есть?

plus 01.11.2020 14:39

Вы сэкономили мне время. Большое спасибо. без него не смог бы пройти тест React.

user1542557 20.05.2021 18:03

Я не рекомендую это решение, оно довольно хакерское. @BadriPaudel Это заменит функцию setState после componentWillUnmount функцией, которая ничего не делает. Функция setState будет по-прежнему вызываться.

Cramer Gabriel 12.07.2021 15:55

У меня была аналогичная проблема и я ее решил:

Я автоматически вводил пользователя в систему, отправляя действие на redux (размещение токена аутентификации в состоянии redux)

а затем я пытался показать сообщение с this.setState ({succ_message: "...") в моем компоненте.

Компонент выглядел пустым с той же ошибкой на консоли: «отключенный компонент» .. «утечка памяти» и т. д.

После того, как я прочитал ответ Уолтера в этой теме

Я заметил, что в таблице маршрутизации моего приложения маршрут моего компонента недействителен, если пользователь вошел в систему:

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

Я сделал Маршрут видимым независимо от того, существует токен или нет.

У меня была аналогичная проблема, спасибо, что @ ford04 мне помог.

Однако произошла другая ошибка.

NB. Я использую перехватчики 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.

What causes the error?

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

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

Это не могло сработать, потому что, несмотря на то, что вы перенаправляете на новую страницу, все состояние и свойства обрабатываются в dom или просто рендеринг на предыдущую страницу не останавливается.

What solution I found

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

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

Хорошее решение, но оно должно быть возвращено <Redirect to = "/ new-route" />

mkane 22.06.2021 17:09

Основываясь на ответе @ 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> ... ;
} ;

Если вы извлекаете данные из axios, а ошибка все еще возникает, просто заключите сеттер в условие

let isRendered = useRef(false);
useEffect(() => {
    isRendered = true;
    axios
        .get("/sample/api")
        .then(res => {
            if (isRendered) {
                setState(res.data);
            }
            return null;
        })
        .catch(err => console.info(err));
    return () => {
        isRendered = false;
    };
}, []);

почему вы не используете isRendered без .current? это особенность?

usertest 11.03.2021 15:22

@usertest Я только что реализовал это решение, но мне пришлось использовать .current.

Mideus 16.04.2021 17:22

В зависимости от того, как вы открываете свою веб-страницу, вы можете не вызывать монтирование. Например, использование <Link/> для возврата к странице, которая уже была смонтирована в виртуальной модели DOM, поэтому перехватывается требование данных из жизненного цикла componentDidMount.

Вы хотите сказать, что componentDidMount() можно вызывать дважды без промежуточного вызова componentWillUnmount() между ними? Я не думаю, что это возможно.

Alexis Wilke 24.12.2020 19:35

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

coder9833idls 04.01.2021 11:13

Существует довольно распространенная ловушка под названием 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); }
    })
  });
  ...
}

Можем ли мы использовать один и тот же крючок с несколькими компонентами?

Ayush Kumar 27.08.2021 06:24

@AyushKumar: да, можно! в этом вся прелесть крючков! состояние isMounted будет специфическим для каждого компонента, который вызывает useIsMounted!

sfletche 04.09.2021 06:43

Думаю, этот способ решения useIsMounted должен быть включен в основной пакет.

dhanushkac 02.11.2021 21:23

Другой вопрос: добавил ли я UseIsMounted в свой хук useEffect и запустил ли я слушатель. Приведет ли добавление return () => в код к какой-либо утечке?

Ayush Kumar 16.11.2021 17:45

Вдохновленный принятым ответом @ 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).

Igor Soloydenko 11.01.2021 19:33

Какое это имеет значение? По крайней мере, я не могу придумать другого поведения.

guneetgstar 11.01.2021 21:46

Вдохновленный ответом @ ford04, я использую этот хук, который также принимает обратные вызовы для успеха, ошибок и, наконец, abortFn:

export const useAsync = (
        asyncFn, 
        onSuccess = false, 
        onError = false, 
        onFinally = false, 
        abortFn = false
    ) => {

    useEffect(() => {
        let isMounted = true;
        const run = async () => {
            try{
                let data = await asyncFn()
                if (isMounted && onSuccess) onSuccess(data)
            } catch(error) {
                if (isMounted && onError) onSuccess(error)
            } finally {
                if (isMounted && onFinally) onFinally()
            }
        }
        run()
        return () => {
            if (abortFn) abortFn()
            isMounted = false
        };
    }, [asyncFn, onSuccess])
}

Если asyncFn выполняет какую-то выборку из серверной части, часто имеет смысл прервать ее, когда компонент отключен (хотя не всегда, иногда, например, если вы загружаете некоторые данные в хранилище, вы также можете просто захотеть доделать, даже если компонент размонтирован)

Подход isMounted в большинстве случаев является анти-шаблоном, потому что он на самом деле ничего не очищает / не отменяет, он просто избегает изменения состояния на несмонтированных компонентах, но ничего не делает с ожидающими асинхронными задачами. Команда React недавно удален предупреждает об утечке, потому что пользователи продолжают создавать множество антишаблонов, чтобы скрыть предупреждение, а не исправить его причину.

Но написать отменяемый код на простом JS может быть очень сложно. Чтобы исправить это, я создал свою собственную библиотеку useAsyncEffect2 с настраиваемыми хуками, построенную на основе отменяемого обещания (c-обещание2) для выполнения отменяемого асинхронного кода для достижения его постепенной отмены. Все асинхронные этапы (обещания), в том числе глубокие, отменяются. Это означает, что запрос здесь будет автоматически прерван, если его родительский контекст будет отменен. Конечно, вместо запроса можно использовать любую другую асинхронную операцию.

    import React, { useState } from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent({url}) {
      const [text, setText] = useState("");
    
      const cancel = useAsyncEffect(
        function* () {
          setText("fetching...");
          const json = (yield cpAxios(url)).data;
          setText(`Success: ${JSON.stringify(json)}`);
        },
        [url]
      );
    
      return (
        <div>
          <div>{text}</div>
          <button onClick = {cancel}>
            Cancel request
          </button>
        </div>
      );
    }
    import React from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    function TestComponent({ url, timeout }) {
      const [cancel, done, result, err] = useAsyncEffect(
        function* () {
          return (yield cpAxios(url).timeout(timeout)).data;
        },
        { states: true, deps: [url] }
      );
    
      return (
        <div>
          {done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
          <button onClick = {cancel} disabled = {done}>
            Cancel async effect (abort request)
          </button>
        </div>
      );
    }
import React, { Component } from "react";
import { ReactComponent } from "c-promise2";
import cpAxios from "cp-axios";

@ReactComponent
class TestComponent extends Component {
  state = {
    text: ""
  };

  *componentDidMount(scope) {
    const { url, timeout } = this.props;
    const response = yield cpAxios(url).timeout(timeout);
    this.setState({ text: JSON.stringify(response.data, null, 2) });
  }

  render() {
    return (<div>{this.state.text}</div>);
  }
}

export default TestComponent;

Еще другие примеры:

const handleClick = async (item: NavheadersType, index: number) => {
    const newNavHeaders = [...navheaders];
    if (item.url) {
      await router.push(item.url);   =>>>> line causing error (causing route to happen)
      // router.push(item.url);  =>>> coreect line
      newNavHeaders.forEach((item) => (item.active = false));
      newNavHeaders[index].active = true;
      setnavheaders([...newNavHeaders]);
    }
  };

Не могли бы вы добавить определение?

Akif 10.02.2021 16:22

Решение от @ ford04 не сработало для меня, и особенно, если вам нужно использовать isMounted в нескольких местах (например, множественное использованиеEffect), рекомендуется использоватьRef, как показано ниже:

  1. Основные пакеты
"dependencies": 
{
  "react": "17.0.1",
}
"devDependencies": { 
  "typescript": "4.1.5",
}

  1. Компонент My Hook
export const SubscriptionsView: React.FC = () => {
  const [data, setData] = useState<Subscription[]>();
  const isMounted = React.useRef(true);

  React.useEffect(() => {
    if (isMounted.current) {
      // fetch data
      // setData (fetch result)

      return () => {
        isMounted.current = false;
      };
    }
  }
});

Согласитесь с точкой зрения, и это решение будет удобнее, и оно обеспечит единый источник истины.

dhanushkac 02.11.2021 21:20

Самое простое и компактное решение (с пояснением) рассматривается ниже как однострочное решение.

useEffect(() => { return () => {}; }, []);

Приведенный выше пример useEffect() возвращает функцию обратного вызова, запускающую React, чтобы завершить отключенную часть своего жизненного цикла до обновления состояния.

Это очень упрощенное решение - все, что нужно. Кроме того, он также работает в отличие от вымышленного синтаксиса, предоставляемого @ ford04 и @sfletche. Между прочим, приведенный ниже фрагмент кода из @ ford04 является чисто воображаемым синтаксисом (@sfletche, @vinod, @guneetgstar и @ Дрю Кордано использовали тот же самый воображаемый синтаксис).

data => { <--- Вымышленный / воображаемый синтаксис

someAsyncOperation().then(data => {
    if (isMounted) setState(data);    // add conditional check
  })

Все мои линтеры и все линтеры моей команды не принимают его и сообщают о Uncaught SyntaxError: unexpected token: '=>'. Я удивлен, что никто не уловил воображаемый синтаксис. Может ли кто-нибудь, кто участвовал в этой цепочке вопросов, особенно среди проголосовавших, объяснить мне, как они заставили решения работать для них?

Ваше утверждение о «воображаемом синтаксисе» ошибочно. Это работает для многих других людей! Если ваш тип возвращаемого значения someAsynchronousOperation() - Promise<void>, то data обязательно вызовет ошибку компиляции TypeScript. Однако, если это Promise<X>, где X не является undefined / void / never, вы определенно сможете использовать .then(data => {...})! Вы не привели полного минимального примера, чтобы рассуждать об этом. Если вы хотите решить конкретную проблему с кодом, откройте отдельный вопрос на StackOverflow. Вы же не хотите получать отрицательные голоса или отмечать ответ.

Igor Soloydenko 27.05.2021 09:38

Вы про стрелочную функцию? Это было введено в ES6. Я не знаю, как вы настроили линтер, но это очень распространенный синтаксис. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…

James Xabregas 19.06.2021 13:37

Ну, я не голосовал против вас ... Не зная ничего о ваших настройках линтера или чего-либо о вашем техническом стеке, я не могу точно сказать, почему это выглядит для вас как синтаксическая ошибка, но, возможно, это потому, что ваш линтер использует настройки для более старых версий Javascript, которые не поддерживают это, но это только предположение.

James Xabregas 29.11.2021 22:43

ОБНОВЛЕНИЕ НЕ ИСПОЛЬЗУЙТЕ МОЙ ОРИГИНАЛЬНЫЙ ОТВЕТ, ЭТО НЕ РАБОТАЕТ

Этот ответ был основан на использовании отменяемых обещаний и примечания в makecancelable, которое я перенес на использование хуков. Однако, похоже, это не отменяет цепочку async / await и даже cancelable-promiseне поддерживает отмену цепочки ожиданий.

Проведя немного дополнительных исследований, выяснилось, что некоторые внутренние причины Google препятствовали тому, чтобы отменяемые обещания вошли в стандарт.

Более того, было некоторое обещание с Bluebird, которое вводит отменяемые обещания, но оно не работает в Expo, или, по крайней мере, я не видел примера его работы в Expo.

принятый ответ - лучший. Поскольку я использую TypeScript, я адаптировал код с несколькими модификациями (я явно установил зависимости, поскольку неявные зависимости принятого ответа, по-видимому, дают цикл повторного рендеринга в моем приложении, добавлены и используют async / await вместо цепочки обещаний, передайте ref на смонтированный объект, чтобы цепочку async / await можно было отменить раньше, если это необходимо)

/**
 * This starts an async function and executes another function that performs
 * React state changes if the component is still mounted after the async
 * operation completes
 * @template T
 * @param {(mountedRef: React.MutableRefObject<boolean>) => Promise<T>} asyncFunction async function,
 *   it has a copy of the mounted ref so an await chain can be canceled earlier.
 * @param {(asyncResult: T) => void} onSuccess this gets executed after async
 *   function is resolved and the component is still mounted
 * @param {import("react").DependencyList} deps
 */
export function useAsyncSetEffect(asyncFunction, onSuccess, deps) {
  const mountedRef = useRef(false);
  useEffect(() => {
    mountedRef.current = true;
    (async () => {
      const x = await asyncFunction(mountedRef);
      if (mountedRef.current) {
        onSuccess(x);
      }
    })();
    return () => {
      mountedRef.current = false;
    };
  }, deps);
}

Оригинальный ответ

Поскольку у меня много разных операций с async, я использую пакет cancelable-promise, чтобы решить эту проблему с минимальными изменениями кода.

Предыдущий код:

useEffect(() => 
  (async () => {
    const bar = await fooAsync();
    setSomeState(bar);
  })(),
  []
);

Новый код:

import { cancelable } from "cancelable-promise";

...

useEffect(
  () => {
    const cancelablePromise = cancelable(async () => {
      const bar = await fooAsync();
      setSomeState(bar);
    })
    return () => cancelablePromise.cancel();
  },
  []
);

Вы также можете включить его в пользовательскую служебную функцию, подобную этой

/**
 * This wraps an async function in a cancelable promise
 * @param {() => PromiseLike<void>} asyncFunction
 * @param {React.DependencyList} deps
 */
export function useCancelableEffect(asyncFunction, deps) {
  useEffect(() => {
    const cancelablePromise = cancelable(asyncFunction());
    return () => cancelablePromise.cancel();
  }, deps);
}

Вот простое решение для этого. Это предупреждение возникает из-за того, что мы выполняем некоторый запрос на выборку, когда этот запрос находится в фоновом режиме (поскольку некоторые запросы занимают некоторое время), и мы возвращаемся с этого экрана, тогда они реагируют, не могут обновить состояние. вот пример кода для этого. написать эта строка перед каждым обновлением состояния.

if (!isScreenMounted.current) return;

Вот полный код

import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
    const isScreenMounted = useRef(true)
    useEffect(() => {
        return () =>  isScreenMounted.current = false
    },[])

    const ConvertFileSubmit = () => {
        if (!isScreenMounted.current) return;
         setUpLoading(true)
 
         var formdata = new FormData();
         var file = {
             uri: `file://${route.params.selectedfiles[0].uri}`,
             type:`${route.params.selectedfiles[0].minetype}`,
             name:`${route.params.selectedfiles[0].displayname}`,
         };
         
         formdata.append("file",file);
         
         fetch(`${BASEURL}/UploadFile`, {
             method: 'POST',
             body: formdata,
             redirect: 'manual'
         }).then(response => response.json())
         .then(result => {
             if (!isScreenMounted.current) return;
             setUpLoading(false)    
         }).catch(error => {
             console.info('error', error)
         });
     }

    return(
    <>
        <StatusBar barStyle = "dark-content" />
        <SafeAreaView>
            <ScrollView
            contentInsetAdjustmentBehavior = "automatic"
            style = {styles.scrollView}>
               <Text>Search Screen</Text>
            </ScrollView>
        </SafeAreaView>
    </>
    )
}

export default SearchScreen;


const styles = StyleSheet.create({
    scrollView: {
        backgroundColor:"red",
    },
    container:{
        flex:1,
        justifyContent:"center",
        alignItems:"center"
    }
})

Я решил эту проблему, предоставив все параметры, которые используются в хуке useEffect

Код сообщил об ошибке:

useEffect(() => {
    getDistrict({
      geonameid: countryId,
      subdistrict: level,
    }).then((res) => {
      ......
    });
  }, [countryId]);

Код после исправления:

useEffect(() => {
    getDistrict({
      geonameid: countryId,
      subdistrict: level,
    }).then((res) => {
      ......
    });
  }, [countryId,level]);

Как видите, проблемы решены после того, как я предоставил все параметры (включая параметр уровня), которые должны пройти.

Добавьте ссылку на компонент jsx, а затем проверьте, что он существует

function Book() {
  const ref = useRef();

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

  return <div ref = {ref}>content</div>
}

Это работает для меня, приложение Nextjs

Bernard Nongpoh 19.09.2021 00:08

Проверка того, смонтирован ли компонент, на самом деле является анти-шаблоном согласно документации React. Решение для предупреждения setState заключается скорее в использовании AbortController.:

useEffect(() => {
  const abortController = new AbortController()   // creating an AbortController
  fetch(url, { signal: abortController.signal })  // passing the signal to the query
    .then(data => {
      setState(data)                              // if everything went well, set the state
    })
    .catch(error => {
      if (error.name === 'AbortError') return     // if the query has been aborted, do nothing
      throw error
    })
  
  return () => {
    abortController.abort()                       // stop the query by aborting on the AbortController on unmount
  }
}, [])

Для асинхронных операций, которые не основаны на Fetch API, все же должен быть способ отменить эти асинхронные операции, и вам лучше использовать их, чем просто проверять, смонтирован ли компонент. Если вы создаете свой собственный API, вы можете реализовать в нем API AbortController для его обработки.

Для большего контекста, проверка того, установлен ли компонент, является анти-шаблоном как React внутренне проверяет, смонтирован ли компонент для отображения этого предупреждения.. Повторное выполнение той же проверки - просто способ скрыть предупреждение, и есть несколько более простых способов скрыть их, чем добавление этого фрагмента кода в большую часть кодовой базы.

Источник: https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934

React уже удалил это предупреждение но вот лучшее решение (не только обходной путь)

useEffect(() => {
  const abortController = new AbortController()   // creating an AbortController
  fetch(url, { signal: abortController.signal })  // passing the signal to the query
    .then(data => {
      setState(data)                              // if everything went well, set the state
    })
    .catch(error => {
      if (error.name === 'AbortError') return     // if the query has been aborted, do nothing
      throw error
    })
  
  return () => {
    abortController.abort() 
  }
}, [])

"If there is a setter hook in your useEffect, return an cleanup function" - me

Проверьте свои функции useEffect(), у которых есть перехватчик установки setSomething(), и заставьте его возвращать функцию очистки в конце. Например:

useEffect(() => {
  ...
  setSomething(something); //
  ...
  return () => { }; // The cleanup function we have to add, it could be empty like this
}, [/*whatever deps or no deps at all*/]);  

У меня есть 2 решения этой ошибки:

  1. возвращение:

Если вы используете hook и useEffect, поставьте конец return на useEffect.

useEffect(() => {
    window.addEventListener('mousemove', logMouseMove)
    return () => {
        window.removeEventListener('mousemove', logMouseMove)
    }
}, [])
  1. componentWillUnmount:

Если вы используете componentDidMount, поставьте рядом с ним componentWillUnmount.

componentDidMount() { 
    window.addEventListener('mousemove', this.logMouseMove)
}

componentWillUnmount() {
    window.removeEventListener('mousemove', this.logMouseMove)
}

Другие вопросы по теме