Saya pikir judulnya sudah mengungkapkan segalanya. Peringatan kuning ditampilkan setiap kali saya melepas komponen yang masih diambil.
MenghiburPeringatan: Tidak dapat memanggil
setState
(atauforceUpdate
) pada komponen yang tidak terpasang. Ini adalah no-op, tapi ... Untuk memperbaikinya, batalkan semua langganan dan tugas asinkron dalamcomponentWillUnmount
metode 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);
});
}
Jawaban:
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
setState
dijalankan 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
Formik
implementasi).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
useRef
yang 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
sumber
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 memanggilcancel()
janji yang dibungkuscomponentWillUnmount
untuk 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.
sumber
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>
sumber
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); });
sumber
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. } }
sumber
this
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
sumber
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); } }
sumber
Selain contoh kait janji yang dapat dibatalkan dalam solusi yang diterima, akan sangat berguna jika memiliki
useAsyncCallback
hook yang membungkus panggilan balik permintaan dan mengembalikan janji yang dapat dibatalkan. Idenya sama, tetapi dengan pengait yang berfungsi seperti biasauseCallback
. 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 }
sumber
Saya pikir saya menemukan jalan keluarnya. Masalahnya bukan pada pengambilannya sendiri, tetapi setState setelah komponen ditutup. Jadi solusinya adalah dengan set
this.state.isMounted
asfalse
dan kemudiancomponentWillMount
ubah menjadi true, dan dicomponentWillUnmount
set ke false lagi. Kemudian hanyaif(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, }) }
sumber