ComponentWillUnmount üzerinde bir getirme nasıl iptal edilir


92

Bence, başlık herşeyi söylüyor. Halen getirilmekte olan bir bileşenin bağlantısını her kaldırdığımda sarı uyarı görüntüleniyor.

Konsol

Uyarı: Bağlanmayan bir bileşende setState(veya forceUpdate) çağrılamaz . Bu işlem yok, ancak ... Düzeltmek için, componentWillUnmountyöntemdeki tüm abonelikleri ve zaman uyumsuz görevleri iptal edin .

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

ne uyarıyor bu sorunum yok
nima moradi

soru güncellendi
João Belo

getirme için söz veya eşzamansız kod mu
verdin

kodu getirmeyi soruya ekle
nima moradi

Yanıtlar:


82

Bir Sözü ateşlediğinizde çözülmesi birkaç saniye sürebilir ve o zamana kadar kullanıcı uygulamanızda başka bir yere gitmiş olabilir. Böylece, Promise çözüldüğünde setState, bağlanmamış bileşen üzerinde yürütüldüğünde ve bir hata alırsınız - tıpkı sizin durumunuzda olduğu gibi. Bu aynı zamanda bellek sızıntılarına da neden olabilir.

Bu nedenle, eşzamansız mantığınızın bir kısmını bileşenlerden çıkarmak en iyisidir.

Aksi takdirde, Sözünüzü bir şekilde iptal etmeniz gerekecektir . Alternatif olarak - son çare tekniği olarak (bu bir antipattern) - bileşenin hala takılı olup olmadığını kontrol etmek için bir değişken tutabilirsiniz:

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}

Bunu tekrar vurgulayacağım - bu bir anti-modeldir ancak sizin durumunuzda yeterli olabilir (tıpkı Formikuygulamada yaptıkları gibi ).

GitHub'da benzer bir tartışma

DÜZENLE:

Muhtemelen aynı problemi (React dışında hiçbir şeyi olmayan) Hooks ile nasıl çözerdim :

SEÇENEK A:

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

SEÇENEK B: Alternatif olarak, bununla useRefbir sınıfın statik özelliği gibi davranan , yani değeri değiştiğinde bileşeni yeniden oluşturmadığı anlamına gelir:

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
}

Örnek: https://codesandbox.io/s/86n1wq2z8


4
bu yüzden, componentWillUnmount üzerindeki getirmeyi iptal etmenin gerçek bir yolu yok mu?
João Belo

1
Oh, cevabınızın kodunu daha önce fark etmemiştim, işe yaradı. teşekkürler
João Belo


2
"Eşzamansız mantığınızı bileşenlerden çıkarmak en iyisi budur." Tepki veren her şey bir bileşen değil mi?
Karpik

1
@Tomasz Mularczyk Çok teşekkür ederim, değerli şeyler yaptınız.
KARTHIKEYAN.

27

React'teki dost canlısı insanlar, getirme çağrılarınızı / vaatlerinizi iptal edilebilir bir sözle tamamlamanızı önerir . Bu dokümantasyonda, getirme ile birlikte kodu sınıftan veya işlevden ayrı tutmak için herhangi bir öneri bulunmamakla birlikte, bu tavsiye edilebilir görünmektedir çünkü diğer sınıflar ve işlevler bu işlevselliğe ihtiyaç duyacaktır, kod çoğaltma bir anti-modeldir ve kalan kod ne olursa olsun imha edilmeli veya iptal edilmelidir componentWillUnmount(). React'e göre, bağlanmamış bir bileşenin durumunu ayarlamaktan kaçınmak cancel()için sarmalanmış sözü çağırabilirsiniz componentWillUnmount.

React'i kılavuz olarak kullanırsak, sağlanan kod bu kod parçacıkları gibi görünecektir:

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

---- DÜZENLE ----

GitHub'daki sorunu takip ederek verilen cevabın tam olarak doğru olmayabileceğini buldum. İşte benim amaçlarım için çalışan kullandığım bir sürüm:

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

Buradaki fikir, çöp toplayıcının işlevi veya kullandığınız her şeyi boş bırakarak hafızayı boşaltmasına yardımcı olmaktı.


github'daki sorunun bağlantısı var mı
Ren

@Ren, sayfayı düzenlemek ve sorunları tartışmak için bir GitHub sitesi var .
haleonj

Artık sorunun o GitHub projesinde tam olarak nerede olduğundan emin değilim.
haleonj

1
GitHub sorununa bağlantı: github.com/facebook/react/issues/5465
sammalfix

23

Bir getirme isteğini iptal etmek için AbortController'ı kullanabilirsiniz .

Ayrıca bkz .: 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
Keşke AbortController gibi istekleri iptal etmek için bir Web API olduğunu bilseydim. Ama pekala, bilmek için çok geç değil. Teşekkür ederim.
Lex Soft

Yani birden fazla fetchesiniz varsa AbortController, hepsini hepsine tekli geçirebilir misiniz ?
serg06

11

Gönderi açıldığından beri, bir "durdurulabilir getirme" eklendi. https://developers.google.com/web/updates/2017/09/abortable-fetch

(belgelerden :)

Kontrolör + sinyal manevrası AbortController ve AbortSignal ile tanışın:

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

Denetleyicinin yalnızca bir yöntemi vardır:

controller.abort (); Bunu yaptığınızda, sinyali bildirir:

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

Bu API, DOM standardı tarafından sağlanır ve bu, API'nin tamamıdır. Bilinçli olarak geneldir, bu nedenle diğer web standartları ve JavaScript kitaplıkları tarafından kullanılabilir.

örneğin, 5 saniye sonra getirme zaman aşımını şu şekilde yapabilirsiniz:

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

İlginç, bu şekilde deneyeceğim. Ancak ondan önce, önce AbortController API'sini okuyacağım.
Lex Soft

Birden fazla getirme için tek bir AbortController örneğini kullanabilir miyiz, öyle ki componentWillUnmount'ta bu tek AbortController'ın abort yöntemini çağırdığımızda, bileşenimizdeki tüm mevcut getirmeleri iptal edecek mi? Değilse, getirmelerin her biri için farklı AbortController örnekleri sağlamamız gerektiği anlamına gelir, değil mi?
Lex Soft

3

Bu uyarının özü, bileşeninizin bazı olağanüstü geri arama / vaatler tarafından tutulan bir referansa sahip olmasıdır.

İkinci modelde yapıldığı gibi isMounted durumunuzu korumanın (bileşeninizi canlı tutan) karşı modelinden kaçınmak için, react web sitesi isteğe bağlı bir söz kullanmayı önerir ; ancak bu kod aynı zamanda nesnenizi canlı tutuyor gibi görünüyor.

Bunun yerine, setState için iç içe geçmiş bağlı bir işlev içeren bir kapatma kullanarak yaptım.

İşte kurucum (typcript)…

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
Bu kavramsal olarak bir isMounted bayrağı tutmaktan farklı değil, sadece onu asmak yerine kapanışa this
bağlıyorsunuz

2

"Tüm abonelikleri ve eşzamanlı olmayanları iptal etmem" gerektiğinde, diğer tüm aboneleri bilgilendirmek için genellikle componentWillUnmount'ta yeniden düzenlemek için bir şeyler gönderirim ve gerekirse sunucuya iptal hakkında bir istek daha gönderirim


2

Sanırım sunucuyu iptal konusunda bilgilendirmek gerekmiyorsa - en iyi yaklaşım sadece async / await sözdizimini kullanmaktır (eğer varsa).

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

Kabul edilen çözümdeki iptal edilebilir vaat kancası örneklerine ek olarak, useAsyncCallbackbir talebi geri aramayı saran ve iptal edilebilir bir sözü geri veren bir kancaya sahip olmak kullanışlı olabilir . Fikir aynı, ancak normal gibi çalışan bir kancayla useCallback. İşte bir uygulama örneği:

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
}

0

CPromise paketini kullanarak , iç içe geçmiş olanlar dahil söz zincirlerinizi iptal edebilirsiniz. AbortController ve jeneratörleri ECMA async işlevlerinin yerini alacak şekilde destekler. CPromise dekoratörlerini kullanarak, eşzamansız görevlerinizi kolayca yönetebilir ve onları iptal edilebilir hale getirebilirsiniz.

Dekoratörler Canlı Demoyu kullanır :

import { async, listen, cancel, timeout } from "c-promise2";
import cpFetch from "cp-fetch";

export class TestComponent extends React.Component {
  state = {
    text: ""
  };

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

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

  @cancel()
  componentWillUnmount() {
    console.log("unmounted");
  }
}

Oradaki tüm aşamalar tamamen iptal edilebilir / iptal edilebilir. İşte React Live Demo ile kullanmanın bir örneği

export class TestComponent extends React.Component {
  state = {};

  async componentDidMount() {
    console.log("mounted");
    this.controller = new CPromise.AbortController();
    try {
      const json = await this.myAsyncTask(
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
      );
      console.log("json:", json);
      await this.myAsyncTaskWithDelay(1000, 123); // just another async task
      this.setState({ text: JSON.stringify(json) });
    } catch (err) {
      if (CPromise.isCanceledError(err)) {
        console.log("tasks terminated");
      }
    }
  }

  myAsyncTask(url) {
    return CPromise.from(function* () {
      const response = yield cpFetch(url); // cancellable request
      return yield response.json();
    }).listen(this.controller.signal);
  }

  myAsyncTaskWithDelay(ms, value) {
    return new CPromise((resolve, reject, { onCancel }) => {
      const timer = setTimeout(resolve, ms, value);
      onCancel(() => {
        console.log("timeout cleared");
        clearTimeout(timer);
      });
    }).listen(this.controller.signal);
  }

  render() {
    return (
      <div>
        AsyncComponent: <span>{this.state.text || "fetching..."}</span>
      </div>
    );
  }
  componentWillUnmount() {
    console.log("unmounted");
    this.controller.abort(); // kill all pending tasks
  }
}

Hook'ları ve cancelyöntemi kullanma

import React, { useEffect, useState } from "react";
import CPromise from "c-promise2";
import cpFetch from "cp-fetch";

export function TestComponent(props) {
  const [text, setText] = useState("fetching...");

  useEffect(() => {
    console.log("mount");
    // all stages here are completely cancellable
    const promise = cpFetch(props.url)
      .then(function* (response) {
        const json = yield response.json();
        setText(`Delay for 2000ms...`);
        yield CPromise.delay(2000);
        setText(`Success: ${JSON.stringify(json)}`);
      })
      .canceled()
      .catch((err) => {
        setText(`Failed: ${err}`);
      });

    return () => {
      console.log("unmount");
      promise.cancel();
    };
  }, [props.url]);

  return <p>{text}</p>;
}

-2

Sanırım bunun bir yolunu buldum. Sorun, getirmenin kendisi değil, bileşen atıldıktan sonraki setState'tir. Çözelti kümesine oldu Yani this.state.isMountedolarak falseve ardından componentWillMounttrue değişim o ve içinde componentWillUnmounttekrar false seti. Sonra if(this.state.isMounted)getirmenin içindeki setState. Şöyle:

  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, state'deki değeri hemen güncellemeyeceği için muhtemelen ideal değildir.
LeonF
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.