Cara membatalkan pengambilan di componentWillUnmount

90

Saya pikir judulnya sudah mengungkapkan segalanya. Peringatan kuning ditampilkan setiap kali saya melepas komponen yang masih diambil.

Menghibur

Peringatan: Tidak dapat memanggil setState(atau forceUpdate) pada komponen yang tidak terpasang. Ini adalah no-op, tapi ... Untuk memperbaikinya, batalkan semua langganan dan tugas asinkron dalam componentWillUnmountmetode ini.

  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
sumber
apa peringatannya saya tidak punya masalah itu
nima moradi
pertanyaan diperbarui
João Belo
apakah Anda berjanji atau kode async untuk diambil
nima moradi
tambahkan Anda mengambil kode ke qustion
nima moradi

Jawaban:

80

Saat Anda mengaktifkan Promise, mungkin perlu waktu beberapa detik sebelum dijawab dan pada saat itu pengguna mungkin sudah menavigasi ke tempat lain di aplikasi Anda. Jadi, ketika Promise resolves setStatedijalankan pada komponen yang tidak di-unmount dan Anda mendapatkan kesalahan - seperti dalam kasus Anda. Ini juga dapat menyebabkan kebocoran memori.

Itulah mengapa yang terbaik adalah memindahkan beberapa logika asinkron Anda dari komponen.

Jika tidak, Anda harus membatalkan Janji Anda . Atau - sebagai teknik pilihan terakhir (ini adalah antipattern) - Anda dapat menyimpan variabel untuk memeriksa apakah komponen masih terpasang:

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}

Saya akan menekankannya lagi - ini adalah antipattern tetapi mungkin cukup dalam kasus Anda (seperti yang mereka lakukan dengan Formikimplementasi).

Diskusi serupa di GitHub

EDIT:

Ini mungkin bagaimana saya menyelesaikan masalah yang sama (tidak memiliki apa-apa selain Bereaksi) dengan Hooks :

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

OPSI B: Alternatifnya useRefyang berperilaku seperti properti statis kelas yang berarti tidak membuat perenderan komponen saat nilainya berubah:

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
}

Contoh: https://codesandbox.io/s/86n1wq2z8

Tomasz Mularczyk
sumber
4
jadi tidak ada cara nyata untuk membatalkan pengambilan di componentWillUnmount?
João Belo
1
Oh, saya tidak melihat kode jawaban Anda sebelumnya, itu berhasil. terima kasih
João Belo
2
apa yang Anda maksud dengan "Itulah mengapa yang terbaik adalah memindahkan logika asinkron Anda dari komponen."? Bukankah semua yang ada di react merupakan sebuah komponen?
Karpik
1
@Tomasz Mularczyk Terima kasih banyak, Anda melakukan hal-hal yang berharga.
KARTHIKEYAN.A
25

Orang-orang yang ramah di React merekomendasikan untuk membungkus panggilan / janji pengambilan Anda dengan janji yang dapat dibatalkan. Meskipun tidak ada rekomendasi dalam dokumentasi tersebut untuk menjaga kode tetap terpisah dari kelas atau fungsi dengan pengambilan, ini tampaknya disarankan karena kelas dan fungsi lain mungkin memerlukan fungsi ini, duplikasi kode adalah anti-pola, dan terlepas dari kode yang tersisa. harus dibuang atau dibatalkan di componentWillUnmount(). Sesuai dengan React, Anda dapat memanggil cancel()janji yang dibungkus componentWillUnmountuntuk menghindari pengaturan status pada komponen yang tidak terpasang.

Kode yang diberikan akan terlihat seperti potongan kode ini jika kita menggunakan React sebagai panduan:

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

---- EDIT ----

Saya telah menemukan jawaban yang diberikan mungkin tidak sepenuhnya benar dengan mengikuti masalah di GitHub. Berikut adalah satu versi yang saya gunakan yang berfungsi untuk tujuan saya:

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

Idenya adalah membantu pengumpul sampah mengosongkan memori dengan membuat fungsi atau apa pun yang Anda gunakan null.

haleonj
sumber
apakah Anda memiliki tautan ke masalah di github
Ren
@Ren, ada situs GitHub untuk mengedit halaman dan mendiskusikan masalah.
haleonj
Saya tidak lagi yakin di mana masalah sebenarnya pada proyek GitHub itu.
haleonj
1
Tautan ke masalah GitHub: github.com/facebook/react/issues/5465
sammalfix
22

Anda dapat menggunakan AbortController untuk membatalkan permintaan pengambilan.

Lihat juga: 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
sumber
2
Saya berharap saya tahu bahwa ada API Web untuk membatalkan permintaan seperti AbortController. Tapi baiklah, belum terlambat untuk mengetahuinya. Terima kasih.
Lex Soft
11

Sejak postingan dibuka, "abortable-fetch" telah ditambahkan. https://developers.google.com/web/updates/2017/09/abortable-fetch

(dari dokumen :)

Manuver pengontrol + sinyal Meet the AbortController dan AbortSignal:

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

Pengontrol hanya memiliki satu metode:

controller.abort (); Ketika Anda melakukan ini, itu memberi tahu sinyal:

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

API ini disediakan oleh standar DOM, dan itu adalah keseluruhan API. Ini sengaja dibuat umum sehingga dapat digunakan oleh standar web lain dan pustaka JavaScript.

misalnya, berikut cara Anda membuat waktu tunggu pengambilan setelah 5 detik:

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);
});
Ben Yitzhaki
sumber
Menarik, saya akan mencoba cara ini. Namun sebelum itu, saya akan membaca AbortController API terlebih dahulu.
Lex Soft
Bisakah kita menggunakan satu contoh AbortController untuk beberapa pengambilan sehingga ketika kita memanggil metode abort dari AbortController tunggal ini di componentWillUnmount, itu akan membatalkan semua pengambilan yang ada di komponen kita? Jika tidak, itu berarti kita harus menyediakan contoh AbortController yang berbeda untuk setiap pengambilan, bukan?
Lex Soft
3

Inti dari peringatan ini adalah bahwa komponen Anda memiliki referensi ke sana yang ditahan oleh beberapa callback / promise yang luar biasa.

Untuk menghindari antipattern menjaga status isMounted Anda (yang membuat komponen Anda tetap hidup) seperti yang dilakukan pada pola kedua, situs web react menyarankan untuk menggunakan sebuah janji opsional ; namun kode itu juga tampaknya membuat objek Anda tetap hidup.

Sebagai gantinya, saya telah melakukannya dengan menggunakan closure dengan fungsi terikat bersarang ke setState.

Ini konstruktor saya (skrip jenis)…

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.
    }
}
Anthony Wieser
sumber
3
Ini secara konseptual tidak berbeda dengan menyimpan bendera isMount, hanya Anda mengikatnya ke penutupan alih-alih menggantungnyathis
AnilRedshift
2

Ketika saya perlu "membatalkan semua langganan dan asinkron", saya biasanya mengirimkan sesuatu ke redux di componentWillUnmount untuk memberi tahu semua pelanggan lain dan mengirim satu permintaan lagi tentang pembatalan ke server jika perlu

Sasha Kos
sumber
2

Saya pikir jika tidak perlu memberi tahu server tentang pembatalan - pendekatan terbaik adalah hanya menggunakan sintaks async / await (jika tersedia).

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);
  }
}
Sasha Kos
sumber
0

Selain contoh kait janji yang dapat dibatalkan dalam solusi yang diterima, akan sangat berguna jika memiliki useAsyncCallbackhook yang membungkus panggilan balik permintaan dan mengembalikan janji yang dapat dibatalkan. Idenya sama, tetapi dengan pengait yang berfungsi seperti biasa useCallback. Berikut contoh implementasinya:

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
}
Thomas Jgenti
sumber
-2

Saya pikir saya menemukan jalan keluarnya. Masalahnya bukan pada pengambilannya sendiri, tetapi setState setelah komponen ditutup. Jadi solusinya adalah dengan set this.state.isMountedas falsedan kemudian componentWillMountubah menjadi true, dan di componentWillUnmountset ke false lagi. Kemudian hanya if(this.state.isMounted)setState di dalam pengambilan. Seperti:

  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,
    })
  }
João Belo
sumber
3
setState mungkin tidak ideal, karena tidak akan segera memperbarui nilai dalam status.
LeonF