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

90

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

Приставка

Предупреждение: невозможно вызвать 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);
      });
  }
Жуан Белу
источник
что это предупреждение, у меня нет этой проблемы
Нима Моради
вопрос обновлен
João Belo
вы обещали или асинхронный код для выборки
Нима Моради
добавьте код для получения кода в запрос
Нима Моради

Ответы:

80

Когда вы запускаете Promise, может пройти несколько секунд, прежде чем оно разрешится, и к тому времени пользователь, возможно, перейдет в другое место в вашем приложении. Поэтому, когда Promise resolves 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;
}

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

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

Томаш Муларчик
источник
4
так что нет реального способа просто отменить выборку на componentWillUnmount?
Жуан Белу
1
Ой, я раньше не замечал код твоего ответа, он работал. спасибо
João Belo
2
что вы имеете в виду, говоря «Вот почему асинхронную логику лучше всего вынести из компонентов»? Разве все в React не является компонентом?
Карпик
1
@Tomasz Mularczyk Большое спасибо, вы сделали достойный материал.
KARTHIKEYAN.A 02
25

Дружелюбные люди в 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.

haleonj
источник
у вас есть ссылка на выпуск на github
Ren
@Ren, есть сайт GitHub для редактирования страницы и обсуждения вопросов.
haleonj 02
Я больше не уверен, в чем именно заключается проблема в этом проекте GitHub.
haleonj 02
1
Ссылка на выпуск GitHub: github.com/facebook/react/issues/5465
sammalfix 07
22

Вы можете использовать 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>

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

Так как пост был открыт, добавлена ​​возможность прерывания. 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);
});
Бен Ицхаки
источник
Интересно, попробую так. Но перед этим я сначала прочитаю AbortController API.
Lex Soft
Можем ли мы использовать только один экземпляр AbortController для нескольких выборок, чтобы, когда мы вызываем метод прерывания этого единственного AbortController в компонентеWillUnmount, он отменял все существующие выборки в нашем компоненте? Если нет, это означает, что мы должны предоставить разные экземпляры AbortController для каждой выборки, верно?
Lex Soft
3

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

Чтобы избежать антипаттерна, заключающегося в поддержании вашего состояния 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.
    }
}
Энтони Визер
источник
3
Концептуально это не отличается от сохранения флага isMounted, только вы привязываете его к закрытию, а не this
вешаете
2

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

Саша Кос
источник
2

Я думаю, что если нет необходимости сообщать серверу об отмене - лучший подход - просто использовать синтаксис 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);
  }
}
Саша Кос
источник
0

В дополнение к примерам перехватов отменяемых обещаний в принятом решении может быть удобно иметь 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
}
Томас Дженти
источник
-2

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