Как отменить выборку на componentWillUnmount

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

Console

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but ... To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

  constructor(props){
    super(props);
    this.state = {
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        this.setState({
          isLoading: false,
          dataSource: responseJson,
        }, function(){
        });
      })
      .catch((error) =>{
        console.error(error);
      });
  }

что это предупреждение, у меня нет этой проблемы

nima moradi 18.04.2018 20:17

вопрос обновлен

João Belo 18.04.2018 20:20

вы обещали или асинхронный код для выборки

nima moradi 18.04.2018 20:24

добавить код получения в запрос

nima moradi 18.04.2018 20:25
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
104
5
79 104
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

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

Когда вы запускаете обещание, может пройти несколько секунд, прежде чем оно разрешится, и к этому времени пользователь может перейти в другое место в вашем приложении. Поэтому, когда Promise разрешает, setState выполняется на отключенном компоненте, и вы получаете сообщение об ошибке - как и в вашем случае. Это также может вызвать утечку памяти.

Вот почему лучше всего вынести часть асинхронной логики из компонентов.

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

componentDidMount(){
  this.mounted = true;

  this.props.fetchData().then((response) => {
    if (this.mounted) {
      this.setState({ data: response })
    }
  })
}

componentWillUnmount(){
  this.mounted = false;
}

Подчеркну еще раз - этот это антипаттерн, но может быть достаточным в вашем случае (как и в случае с реализацией Formik).

Аналогичное обсуждение на GitHub

Обновлено:

Вероятно, вот как я могу решить ту же проблему (не имея ничего, кроме React) с Хуки:

ВАРИАНТ А:

import React, { useState, useEffect } from "react";

export default function Page() {
  const value = usePromise("https://something.com/api/");
  return (
    <p>{value ? value : "fetching data..."}</p>
  );
}

function usePromise(url) {
  const [value, setState] = useState(null);

  useEffect(() => {
    let isMounted = true; // track whether component is mounted

    request.get(url)
      .then(result => {
        if (isMounted) {
          setState(result);
        }
      });

    return () => {
      // clean up
      isMounted = false;
    };
  }, []); // only on "didMount"

  return value;
}

ВАРИАНТ Б: В качестве альтернативы useRef, который ведет себя как статическое свойство класса, что означает, что он не выполняет повторную визуализацию компонента при изменении его значения:

function usePromise2(url) {
  const isMounted = React.useRef(true)
  const [value, setState] = useState(null);


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

  useEffect(() => {
    request.get(url)
      .then(result => {
        if (isMounted.current) {
          setState(result);
        }
      });
  }, []);

  return value;
}

// or extract it to custom hook:
function useIsMounted() {
  const isMounted = React.useRef(true)

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

  return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}

Пример: https://codesandbox.io/s/86n1wq2z8

так что нет реального способа просто отменить выборку для componentWillUnmount?

João Belo 18.04.2018 20:32

@ JoãoBelo Я не уверен, поддерживается ли это. Взгляните в этом пакете

Tomasz Mularczyk 18.04.2018 20:36

О, я раньше не замечал код вашего ответа, он сработал. Благодарность

João Belo 18.04.2018 21:09

Теперь необходимо присвоить переменной isMounted другое имя, иначе вы получите ошибку setting getter-only property .

dude 26.06.2018 17:34

что вы имеете в виду, говоря «Вот почему асинхронную логику лучше всего вынести из компонентов»? Разве все в React не является компонентом?

Karpik 14.09.2018 18:00

@Karpik, я имею в виду использование redux или mobx или другую библиотеку управления состоянием. Однако новые функции, такие как задержка реакции, могут решить эту проблему.

Tomasz Mularczyk 14.09.2018 18:15

@Tomasz Mularczyk Большое спасибо, вы сделали достойный материал.

KARTHIKEYAN.A 02.11.2018 13:27

Также смотрите FAQ по React Hooks для пример.

ford04 01.09.2019 14:24

Использование управляющей переменной, такой как this.mounted, которую вы использовали, - это именно то, что я думал в качестве первого решения. Я на самом деле ищу другие решения, но, поскольку вы упомянули об этом в первый раз, я думаю, что буду использовать его. Спасибо за косвенное подтверждение той же идеи, что и моя.

Lex Soft 19.02.2020 13:28

Решение React Hooks лучше, чем первое решение? Он использует новую причудливую функцию, но в основном работает так же.

sdgfsdh 28.02.2020 15:47

Предупреждение: isMounted (...) устарел в простых классах JavaScript React.

Nijat Aliyev 23.08.2020 10:22

помимо того, что это анти-шаблон, первое решение не работает, потому что setState является АСИНХРОННЫМ

Marcus Junius Brutus 18.02.2021 18:20

Внимательно прочтите ссылку на антишаблон. Использование метода isMounted() является анти-шаблоном, но рекомендуется использовать свойство, подобное _isMounted.

haleonj 20.03.2021 15:41

Когда мне нужно «отменить все подписки и асинхронно», я обычно отправляю что-то в redux в componentWillUnmount, чтобы проинформировать всех других подписчиков и при необходимости отправить еще один запрос об отмене на сервер.

Думаю, я придумал способ обойти это. Проблема не столько в самой выборке, сколько в setState после закрытия компонента. Итак, решение заключалось в том, чтобы установить this.state.isMounted как false, а затем на componentWillMount изменить его на true, а в componentWillUnmount снова установить на false. Затем просто if (this.state.isMounted) setState внутри выборки. Вот так:

  constructor(props){
    super(props);
    this.state = {
      isMounted: false,
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    this.setState({
      isMounted: true,
    })

    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        if (this.state.isMounted){
          this.setState({
            isLoading: false,
            dataSource: responseJson,
          }, function(){
          });
        }
      })
      .catch((error) =>{
        console.error(error);
      });
  }

  componentWillUnmount() {
    this.setState({
      isMounted: false,
    })
  }

setState, вероятно, не идеален, поскольку он не обновляет значение в состоянии немедленно.

LeonF 11.05.2018 18:25

Суть этого предупреждения в том, что у вашего компонента есть ссылка на него, удерживаемая каким-то невыполненным обратным вызовом / обещанием.

Чтобы избежать антипаттерна сохранения вашего состояния isMounted (которое поддерживает работу вашего компонента), как это было сделано во втором шаблоне, сайт реакции предлагает используя необязательное обещание; однако этот код, кажется, также поддерживает ваш объект в живых.

Вместо этого я сделал это, используя закрытие с вложенной функцией привязки к setState.

Вот мой конструктор (машинописный текст)…

constructor(props: any, context?: any) {
    super(props, context);

    let cancellable = {
        // it's important that this is one level down, so we can drop the
        // reference to the entire object by setting it to undefined.
        setState: this.setState.bind(this)
    };

    this.componentDidMount = async () => {
        let result = await fetch(…);            
        // ideally we'd like optional chaining
        // cancellable.setState?.({ url: result || '' });
        cancellable.setState && cancellable.setState({ url: result || '' });
    }

    this.componentWillUnmount = () => {
        cancellable.setState = undefined; // drop all references.
    }
}

Это концептуально ничем не отличается от сохранения флага isMounted, только вы привязываете его к закрытию, а не навешиваете его на this.

AnilRedshift 19.10.2018 20:48

Дружелюбные люди из React рекомендую оборачивают ваши вызовы / обещания fetch в отменяемые обещания. Хотя в этой документации нет рекомендаций по хранению кода отдельно от класса или функции с выборкой, это кажется целесообразным, потому что другие классы и функции могут нуждаться в этой функциональности, дублирование кода является анти-шаблоном и независимо от устаревшего кода. следует утилизировать или аннулировать в componentWillUnmount(). Согласно React, вы можете вызвать cancel() для обернутого обещания в componentWillUnmount, чтобы избежать установки состояния на размонтированном компоненте.

Предоставленный код будет выглядеть примерно так, если мы воспользуемся React в качестве руководства:

const makeCancelable = (promise) => {
    let hasCanceled_ = false;

    const wrappedPromise = new Promise((resolve, reject) => {
        promise.then(
            val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
            error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel() {
            hasCanceled_ = true;
        },
    };
};

const cancelablePromise = makeCancelable(fetch('LINK HERE'));

constructor(props){
    super(props);
    this.state = {
        isLoading: true,
        dataSource: [{
            name: 'loading...',
            id: 'loading',
        }]
    }
}

componentDidMount(){
    cancelablePromise.
        .then((response) => response.json())
        .then((responseJson) => {
            this.setState({
                isLoading: false,
                dataSource: responseJson,
            }, () => {

            });
        })
        .catch((error) =>{
            console.error(error);
        });
}

componentWillUnmount() {
    cancelablePromise.cancel();
}

---- РЕДАКТИРОВАТЬ ----

Я обнаружил, что данный ответ может быть не совсем правильным, следя за проблемой на GitHub. Вот одна из версий, которые я использую, и она подходит для моих целей:

export const makeCancelableFunction = (fn) => {
    let hasCanceled = false;

    return {
        promise: (val) => new Promise((resolve, reject) => {
            if (hasCanceled) {
                fn = null;
            } else {
                fn(val);
                resolve(val);
            }
        }),
        cancel() {
            hasCanceled = true;
        }
    };
};

Идея заключалась в том, чтобы помочь сборщику мусора освободить память, сделав функцию или что-то еще, что вы используете, null.

у вас есть ссылка на проблему на github

Ren 01.11.2018 04:27

@Ren, есть GitHub сайт для редактирования страницы и обсуждения вопросов.

haleonj 02.11.2018 13:12

Я больше не уверен, в чем именно заключается проблема в этом проекте GitHub.

haleonj 02.11.2018 13:13

Ссылка на выпуск GitHub: github.com/facebook/react/issues/5465

sammalfix 07.12.2018 09:56

Вы можете использовать AbortController для отмены запроса на выборку.

См. Также: https://www.npmjs.com/package/abortcontroller-polyfill

class FetchComponent extends React.Component{
  state = { todos: [] };
  
  controller = new AbortController();
  
  componentDidMount(){
    fetch('https://jsonplaceholder.typicode.com/todos',{
      signal: this.controller.signal
    })
    .then(res => res.json())
    .then(todos => this.setState({ todos }))
    .catch(e => alert(e.message));
  }
  
  componentWillUnmount(){
    this.controller.abort();
  }
  
  render(){
    return null;
  }
}

class App extends React.Component{
  state = { fetch: true };
  
  componentDidMount(){
    this.setState({ fetch: false });
  }
  
  render(){
    return this.state.fetch && <FetchComponent/>
  }
}

ReactDOM.render(<App/>, document.getElementById('root'))
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id = "root"></div>

Хотел бы я знать, что существует веб-API для отмены запросов, таких как AbortController. Но ладно, еще не поздно это узнать. Спасибо.

Lex Soft 19.02.2020 14:05

Итак, если у вас несколько fetch, можете ли вы передать один AbortController всем им?

serg06 11.12.2020 00:42

Поскольку сообщение было открыто, добавлена ​​возможность прерывания. https://developers.google.com/web/updates/2017/09/abortable-fetch

(из документов :)

Контроллер + сигнальный маневр Встречайте AbortController и AbortSignal:

const controller = new AbortController();
const signal = controller.signal;

У контроллера есть только один метод:

controller.abort (); Когда вы это сделаете, он уведомит сигнал:

signal.addEventListener('abort', () => {
  // Logs true:
  console.info(signal.aborted);
});

Этот API предоставляется стандартом DOM, и это весь API. Он намеренно универсален, поэтому может использоваться другими веб-стандартами и библиотеками JavaScript.

например, вот как можно установить тайм-аут выборки через 5 секунд:

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.info(text);
});

Интересно, попробую так. Но перед этим я сначала прочту AbortController API.

Lex Soft 19.02.2020 13:55

Можем ли мы использовать только один экземпляр AbortController для нескольких выборок, чтобы, когда мы вызываем метод прерывания этого единственного AbortController в компонентеWillUnmount, он отменял все существующие выборки в нашем компоненте? Если нет, это означает, что мы должны предоставить разные экземпляры AbortController для каждой выборки, верно?

Lex Soft 22.02.2020 12:03

@LexSoft вы нашли ответ на свой вопрос?

Superdude 14.03.2021 09:09

@Superdude ответ - да

McMurphy 22.03.2021 06:31

Я думаю, что если нет необходимости сообщать серверу об отмене - лучший подход - просто использовать синтаксис async / await (если он доступен).

constructor(props){
  super(props);
  this.state = {
    isLoading: true,
    dataSource: [{
      name: 'loading...',
      id: 'loading',
    }]
  }
}

async componentDidMount() {
  try {
    const responseJson = await fetch('LINK HERE')
      .then((response) => response.json());

    this.setState({
      isLoading: false,
      dataSource: responseJson,
    }
  } catch {
    console.error(error);
  }
}

В дополнение к примерам перехватчиков отменяемых обещаний в принятом решении может быть удобно иметь ловушку useAsyncCallback, которая обертывает обратный вызов запроса и возвращает отменяемое обещание. Идея та же, но с крючком, работающим как обычный useCallback. Вот пример реализации:

function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[]) {
  const isMounted = useRef(true)

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

  const cb = useCallback(callback, dependencies)

  const cancellableCallback = useCallback(
    (...args: any[]) =>
      new Promise<T>((resolve, reject) => {
        cb(...args).then(
          value => (isMounted.current ? resolve(value) : reject({ isCanceled: true })),
          error => (isMounted.current ? reject(error) : reject({ isCanceled: true }))
        )
      }),
    [cb]
  )

  return cancellableCallback
}

Используя пакет CPromise, вы можете отменить цепочки обещаний, в том числе вложенные. Он поддерживает AbortController и генераторы в качестве замены асинхронных функций ECMA. Используя декораторы CPromise, вы можете легко управлять своими асинхронными задачами, делая их отменяемыми.

Использование декораторов Живая демонстрация:

import React from "react";
import { ReactComponent, timeout } from "c-promise2";
import cpFetch from "cp-fetch";

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

  @timeout(5000)
  *componentDidMount() {
    console.info("mounted");
    const response = yield cpFetch(this.props.url);
    this.setState({ text: `json: ${yield response.text()}` });
  }

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

  componentWillUnmount() {
    console.info("unmounted");
  }
}

Все этапы полностью отменяются / отменяются. Вот пример использования его с React Живая демонстрация

import React, { Component } from "react";
import {
  CPromise,
  CanceledError,
  ReactComponent,
  E_REASON_UNMOUNTED,
  listen,
  cancel
} from "c-promise2";
import cpAxios from "cp-axios";

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

  *componentDidMount(scope) {
    console.info("mount");
    scope.onCancel((err) => console.info(`Cancel: ${err}`));
    yield CPromise.delay(3000);
  }

  @listen
  *fetch() {
    this.setState({ text: "fetching..." });
    try {
      const response = yield cpAxios(this.props.url).timeout(
        this.props.timeout
      );
      this.setState({ text: JSON.stringify(response.data, null, 2) });
    } catch (err) {
      CanceledError.rethrow(err, E_REASON_UNMOUNTED);
      this.setState({ text: err.toString() });
    }
  }

  *componentWillUnmount() {
    console.info("unmount");
  }

  render() {
    return (
      <div className = "component">
        <div className = "caption">useAsyncEffect demo:</div>
        <div>{this.state.text}</div>
        <button
          className = "btn btn-success"
          type = "submit"
          onClick = {() => this.fetch(Math.round(Math.random() * 200))}
        >
          Fetch random character info
        </button>
        <button
          className = "btn btn-warning"
          onClick = {() => cancel.call(this, "oops!")}
        >
          Cancel request
        </button>
      </div>
    );
  }
}

Использование хуков и метода cancel

import React, { useState } from "react";
import {
  useAsyncEffect,
  E_REASON_UNMOUNTED,
  CanceledError
} from "use-async-effect2";
import cpAxios from "cp-axios";

export default function TestComponent(props) {
  const [text, setText] = useState("");
  const [id, setId] = useState(1);

  const cancel = useAsyncEffect(
    function* () {
      setText("fetching...");
      try {
        const response = yield cpAxios(
          `https://rickandmortyapi.com/api/character/${id}`
        ).timeout(props.timeout);
        setText(JSON.stringify(response.data, null, 2));
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED);
        setText(err.toString());
      }
    },
    [id]
  );

  return (
    <div className = "component">
      <div className = "caption">useAsyncEffect demo:</div>
      <div>{text}</div>
      <button
        className = "btn btn-success"
        type = "submit"
        onClick = {() => setId(Math.round(Math.random() * 200))}
      >
        Fetch random character info
      </button>
      <button className = "btn btn-warning" onClick = {cancel}>
        Cancel request
      </button>
    </div>
  );
}

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

поскольку мы знаем, что функция также является объектом в js, поэтому мы можем использовать их для обновления значений закрытия

const promesifiedFunction1 = (func) => {
  return function promesify(...agrs){
    let cancel = false;
    promesify.abort = ()=>{
      cancel = true;
    }
    return new Promise((resolve, reject)=>{
       function callback(error, value){
          if (cancel){
              reject({cancel:true})
          }
          error ? reject(error) : resolve(value);
       }
       agrs.push(callback);
       func.apply(this,agrs)
    })
  }
}

//here param func pass as callback should return a promise object
//example fetch browser API
//const fetchWithAbort = promesifiedFunction2(fetch)
//use it as fetchWithAbort('http://example.com/movies.json',{...options})
//later in componentWillUnmount fetchWithAbort.abort()
const promesifiedFunction2 = (func)=>{
  return async function promesify(...agrs){
    let cancel = false;
    promesify.abort = ()=>{
      cancel = true;
    }

    try {
      const fulfilledValue = await func.apply(this,agrs);
      if (cancel){
        throw 'component un mounted'
      }else{
        return fulfilledValue;
      }
    }
    catch (rejectedValue) {
      return rejectedValue
    }
  }
}

тогда внутри componentWillUnmount () просто вызовите promesifiedFunction.abort () это обновит флаг отмены и запустит функцию отклонения

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