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

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

Приставка

Предупреждение. Невозможно вызвать setState (или forceUpdate) для неустановленного компонента. Это не работает, но... Чтобы исправить, отмените все подписки и асинхронные задачи в методе componentWillUnmount.

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

Ответы

Ответ 1

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

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

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

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}

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

Подобное обсуждение на GitHub

EDIT:

Вероятно, именно так я бы решил ту же проблему (не имея ничего, кроме 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;
}

ВАРИАНТ B: В качестве альтернативы 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

Ответ 2

Приветливые люди в React рекомендуют упаковывать ваши звонки/обещания в отменяемое обещание. Хотя в этой документации нет рекомендаций хранить код отдельно от класса или функции с извлечением, это представляется целесообразным, поскольку другие классы и функции, вероятно, нуждаются в этой функции, дублирование кода является анти-паттерном и, несмотря на длительный код должен быть удален или отменен в 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;
        }
    };
};

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

Ответ 3

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

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>

Ответ 4

С тех пор, как сообщение было открыто, была добавлена "abortable-fetch". 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.log(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.log(text);
});

Ответ 5

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

Ответ 6

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

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

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

Здесь мой конструктор (машинопись)...

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

    let cancellable = {
        // it 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.
    }
}

Ответ 7

Думаю, я решил обойти это. Проблема заключается не столько в получении, а в 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,
    })
  }

Ответ 8

Я использую axios, чтобы сделать запрос к моему API:

componentDidMount() вызывается сразу после монтирования компонента (вставки в дерево)

componentWillUnmount() вызывается непосредственно перед размонтированием и уничтожением компонента

export default class index extends Component{

_isMounted = false;

 state = { 
          produtos : undefined
         }

componentDidMount(){
   this._isMounted=true;
}

componentWillUnmount(){
   this._isMounted = false;
}

onclick(){
    async function get(){

      await api.post('https://yourapi/Products/Select',{cod_Produto:cod_produto})
                .then(function(response){
                      if (this._isMounted===true){
                      this.setState({produtos:undefined});                                                                                          
                });      
    }

    get();
}

Шрифт: 1 - https://www.robinwieruch.de/react-warning-cant-call-setstate-on-an-unmounted-component.

Шрифт: 2 - https://reactjs.org/docs/react-component.html.

Вардар:https://github.com/axios/axios