Як скасувати вибірку на компонентіWillUnmount


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

що це застереження, у мене немає цього випуску
nima moradi

питання оновлено
João Belo

ви обіцяли або асинхронний код для отримання
німа мораді

додати ваш код до qustion
nima moradi

Відповіді:


80

Коли ви запускаєте обіцянку, може пройти кілька секунд, перш ніж вона вирішиться, і до того часу користувач міг перейти до іншого місця у вашій програмі. Отже, коли 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: В якості альтернативи, 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


4
так що немає реального способу просто скасувати вибірку на компонентіWillUnmount?
João Belo

1
О, я раніше не помічав коду вашої відповіді, він спрацював. подяка
Жоау Белу


2
що ви маєте на увазі під "Ось чому найкраще перенести свою асинхронну логіку з компонентів."? Чи не все в реакції є компонентом?
Карпік,

1
@Tomasz Mularczyk Щиро дякую, ти зробив гідні речі.
KARTHIKEYAN.

25

Доброзичливі люди в 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;
        }
    };
};

Ідея полягала у тому, щоб допомогти збиральнику сміття звільнити пам’ять, зробивши функцію або те, що ви використовуєте, нульовим.


у вас є посилання на випуск на github
Ren

@Ren, існує сайт GitHub для редагування сторінки та обговорення питань.
haleonj

Я вже не впевнений, де саме проблема в цьому проекті GitHub.
haleonj

1
Посилання на випуск GitHub: github.com/facebook/react/issues/5465
sammalfix

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>


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

Цікаво, я спробую таким чином. Але до цього я спочатку прочитаю API AbortController.
Lex Soft

Чи можемо ми використовувати лише один екземпляр AbortController для кількох виборів, так що, коли ми викликаємо метод abort цього єдиного AbortController у компонентіWillUnmount, він скасує всі існуючі вилучення в нашому компоненті? Якщо ні, це означає, що ми повинні надавати різні екземпляри AbortController для кожного з вибору, чи не так?
Lex Soft

3

Суть цього попередження полягає в тому, що ваш компонент має посилання на нього, яке утримується деяким видатним зворотним викликом / обіцянкою.

Щоб уникнути антипаттерну збереження вашого стану isMount (який підтримує ваш компонент живим), як це було зроблено за другим шаблоном, веб-сайт React пропонує використовувати необов’язкову обіцянку ; однак цей код також, здається, підтримує ваш об'єкт живим.

Натомість я зробив це, використовуючи закриття з вкладеною прив’язаною функцією 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
Це концептуально нічим не відрізняється від збереження прапора isMount, тільки ви прив'язуєте його до закриття, а не this
повішаєте

2

Коли мені потрібно "скасувати всі підписки та асинхронні", я зазвичай відправляю щось для зменшення в 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, а в componentWillUnmountset знову на 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.