
Pertimbangkan penerapan permintaan data ke API menggunakan teman baru React Hooks dan teman lama yang baik Render Prop dan HOC (Komponen Pesanan Tinggi). Cari tahu apakah teman baru benar-benar lebih baik daripada yang lama.
Hidup tidak tinggal diam, React berubah menjadi lebih baik. Pada bulan Februari 2019, React Hooks muncul di React 16.8.0. Sekarang dalam komponen fungsional Anda dapat bekerja dengan keadaan lokal dan melakukan efek samping. Tidak ada yang percaya bahwa itu mungkin, tetapi semua orang selalu menginginkannya. Jika Anda tidak mengetahui detail, klik di sini untuk detailnya.
React Hooks memungkinkan untuk akhirnya meninggalkan pola seperti HOC dan Render Prop. Karena selama penggunaan, sejumlah klaim telah menumpuk terhadap mereka:
Agar tidak berdasar, mari kita lihat contoh bagaimana React Hooks lebih baik (atau mungkin lebih buruk) Render Prop. Kami akan mempertimbangkan Render Prop, bukan HOC, karena dalam penerapannya sangat mirip dan HOC memiliki lebih banyak kelemahan. Mari kita coba menulis utilitas yang memproses permintaan data ke API. Saya yakin banyak yang menulis ini dalam kehidupan mereka ratusan kali, yah, mari kita lihat apakah itu mungkin lebih baik dan lebih mudah.
Untuk ini kita akan menggunakan perpustakaan aksioma populer. Dalam skenario paling sederhana, Anda perlu memproses status berikut:
- proses akuisisi data (isFetching)
- data berhasil diterima (responseData)
- kesalahan menerima data (kesalahan)
- pembatalan permintaan, jika dalam pelaksanaannya parameter permintaan telah berubah, dan Anda perlu mengirim yang baru
- membatalkan permintaan jika komponen ini tidak lagi di DOM
1. Skenario sederhana
Kami akan menulis status default dan fungsi (reducer) yang berubah tergantung pada hasil permintaan: sukses / kesalahan.
Apa itu Peredam?Untuk referensi. Reducer datang kepada kami dari pemrograman fungsional, dan untuk sebagian besar pengembang JS dari Redux. Ini adalah fungsi yang mengambil status sebelumnya dan tindakan serta mengembalikan status berikutnya.
const defaultState = { responseData: null, isFetching: true, error: null }; function reducer1(state, action) { switch (action.type) { case "fetched": return { ...state, isFetching: false, responseData: action.payload }; case "error": return { ...state, isFetching: false, error: action.payload }; default: return state; } }
Kami menggunakan kembali fungsi ini dalam dua pendekatan.
Render prop
class RenderProp1 extends React.Component { state = defaultState; axiosSource = null; tryToCancel() { if (this.axiosSource) { this.axiosSource.cancel(); } } dispatch(action) { this.setState(prevState => reducer(prevState, action)); } fetch = () => { this.tryToCancel(); this.axiosSource = axios.CancelToken.source(); axios .get(this.props.url, { cancelToken: this.axiosSource.token }) .then(response => { this.dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { this.dispatch({ type: "error", payload: error }); }); }; componentDidMount() { this.fetch(); } componentDidUpdate(prevProps) { if (prevProps.url !== this.props.url) { this.fetch(); } } componentWillUnmount() { this.tryToCancel(); } render() { return this.props.children(this.state); }
Bereaksi kait
const useRequest1 = url => { const [state, dispatch] = React.useReducer(reducer, defaultState); React.useEffect(() => { const source = axios.CancelToken.source(); axios .get(url, { cancelToken: source.token }) .then(response => { dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { dispatch({ type: "error", payload: error }); }); return source.cancel; }, [url]); return [state]; };
Dengan url, dari komponen yang digunakan, kami mendapatkan data - axios.get (). Kami memproses kesuksesan dan kesalahan, mengubah status melalui pengiriman (tindakan). Kembalikan status ke komponen. Dan jangan lupa untuk membatalkan permintaan jika url berubah atau jika komponen dihapus dari DOM. Ini sederhana, tetapi Anda dapat menulis dengan berbagai cara. Kami menyoroti pro dan kontra dari dua pendekatan:
React Hooks memungkinkan Anda untuk menulis lebih sedikit kode, dan ini adalah fakta yang tidak terbantahkan. Ini berarti bahwa efektivitas Anda sebagai pengembang semakin meningkat. Tetapi Anda harus menguasai paradigma baru.
Ketika ada nama siklus hidup komponen, semuanya sangat jelas. Pertama, kami mendapatkan data setelah komponen muncul di layar (componentDidMount), lalu kami mendapatkannya lagi jika props.url telah berubah dan sebelum itu kami tidak lupa untuk membatalkan permintaan sebelumnya (componentDidUpdate), jika komponen telah dihapus dari DOM, kemudian membatalkan permintaan (componentWillUnmount) .
Tetapi sekarang kami menyebabkan efek samping secara langsung dalam render, kami diajari bahwa ini tidak mungkin. Meski berhenti, tidak benar-benar di render. Dan di dalam fungsi useEffect, yang akan melakukan sesuatu yang tidak sinkron setelah setiap render, atau lebih tepatnya komit dan render DOM baru.
Tetapi kami tidak perlu setelah setiap render, tetapi hanya pada render pertama dan dalam hal mengubah url, yang kami indikasikan sebagai argumen kedua untuk menggunakanEffect.
Paradigma baruMemahami bagaimana React Hooks bekerja membutuhkan kesadaran akan hal-hal baru. Misalnya, perbedaan antara fase: komit dan render. Dalam fase render, React menghitung perubahan mana yang berlaku di DOM dengan membandingkan dengan hasil render sebelumnya. Dan dalam fase komit, Bereaksi menerapkan perubahan ini ke DOM. Dalam fase komit metode disebut: componentDidMount dan componentDidUpdate. Tetapi apa yang tertulis dalam useEffect akan dipanggil setelah komit secara asinkron dan, oleh karena itu, tidak akan memblokir rendering DOM jika Anda tiba-tiba secara tidak sengaja memutuskan untuk menyinkronkan banyak hal dalam efek samping.
Kesimpulan - gunakan useEffect. Menulis lebih sedikit dan lebih aman.
Dan satu lagi fitur hebat: useEffect dapat membersihkan setelah efek sebelumnya dan setelah mengeluarkan komponen dari DOM. Terima kasih kepada Rx yang mengilhami tim Bereaksi untuk pendekatan ini.
Menggunakan utilitas kami dengan React Hooks juga jauh lebih nyaman.
const AvatarRenderProp1 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`}> {state => { if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; }} </RenderProp> );
const AvatarWithHook1 = ({ username }) => { const [state] = useRequest(`https://api.github.com/users/${username}`); if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; };
Opsi React Hooks sekali lagi terlihat lebih ringkas dan jelas.
Cons Render Prop:
1) tidak jelas apakah tata letak ditambahkan atau hanya logika
2) jika Anda perlu memproses status dari Render Prop di negara bagian setempat atau dalam siklus hidup komponen anak, Anda harus membuat komponen baru
Tambahkan fungsi baru - menerima data dengan parameter baru dengan tindakan pengguna. Saya ingin, misalnya, tombol yang mendapat avatar pengembang favorit Anda.
2) Memperbarui data tindakan pengguna
Tambahkan tombol yang mengirim permintaan dengan nama pengguna baru. Solusi paling sederhana adalah dengan menyimpan nama pengguna di negara bagian komponen dan mentransfer nama pengguna baru dari negara, bukan alat peraga seperti sekarang. Tetapi kemudian kita akan memiliki copy-paste di mana pun kita membutuhkan fungsionalitas serupa. Jadi kami menempatkan fungsi ini ke dalam utilitas kami.
Kami akan menggunakannya seperti ini:
const Avatar2 = ({ username }) => { ... <button onClick={() => update("https://api.github.com/users/NewUsername")} > Update avatar for New Username </button> ... };
Mari kita tulis implementasi. Di bawah ini hanya tertulis perubahan dibandingkan dengan versi aslinya.
function reducer2(state, action) { switch (action.type) { ... case "update url": return { ...state, isFetching: true, url: action.payload, defaultUrl: action.payload }; case "update url manually": return { ...state, isFetching: true, url: action.payload, defaultUrl: state.defaultUrl }; ... } }
Render prop
class RenderProp2 extends React.Component { state = { responseData: null, url: this.props.url, defaultUrl: this.props.url, isFetching: true, error: null }; static getDerivedStateFromProps(props, state) { if (state.defaultUrl !== props.url) { return reducer(state, { type: "update url", payload: props.url }); } return null; } ... componentDidUpdate(prevProps, prevState) { if (prevState.url !== this.state.url) { this.fetch(); } } ... update = url => { this.dispatch({ type: "update url manually", payload: url }); }; render() { return this.props.children(this.state, this.update); } }
Bereaksi kait
const useRequest2 = url => { const [state, dispatch] = React.useReducer(reducer, { url, defaultUrl: url, responseData: null, isFetching: true, error: null }); if (url !== state.defaultUrl) { dispatch({ type: "update url", payload: url }); } React.useEffect(() => { …(fetch data); }, [state.url]); const update = React.useCallback( url => { dispatch({ type: "update url manually", payload: url }); }, [dispatch] ); return [state, update]; };
Jika Anda hati-hati melihat kode, Anda perhatikan:
- url mulai disimpan di dalam utilitas kami;
- defaultUrl muncul untuk mengidentifikasi bahwa url telah diperbarui melalui alat peraga. Kami perlu memantau perubahan props.url, jika tidak permintaan baru tidak akan dikirimkan;
- menambahkan fungsi pembaruan, yang kami kembali ke komponen untuk mengirim permintaan baru dengan mengklik tombol.
Catatan dengan Render Prop, kami harus menggunakan getDerivedStateFromProps untuk memperbarui keadaan lokal jika ada perubahan props.url. Dan dengan React Hooks tanpa abstraksi baru, Anda dapat segera menghubungi pembaruan status di render - hore, kawan, akhirnya!
Satu-satunya komplikasi dengan React Hooks adalah memoit fungsi pembaruan sehingga tidak akan berubah di antara pembaruan komponen. Ketika, seperti dalam Prop Render, fungsi pembaruan adalah metode kelas.
3) Polling API pada interval atau Polling yang sama
Mari kita tambahkan fitur populer lainnya. Terkadang Anda perlu terus-menerus menanyakan API. Anda tidak pernah tahu pengembang favorit Anda mengubah gambar profil, dan Anda tidak tahu. Tambahkan parameter interval.
Penggunaan:
const AvatarRenderProp3 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`} pollInterval={1000}> ...
const AvatarWithHook3 = ({ username }) => { const [state, update] = useRequest( `https://api.github.com/users/${username}`, 1000 ); ...
Implementasi:
function reducer3(state, action) { switch (action.type) { ... case "poll": return { ...state, requestId: state.requestId + 1, isFetching: true }; ... } }
Render prop
class RenderProp3 extends React.Component { state = { ... requestId: 1, } ... timeoutId = null; ... tryToClearTimeout() { if (this.timeoutId) { clearTimeout(this.timeoutId); } } poll = () => { this.tryToClearTimeout(); this.timeoutId = setTimeout(() => { this.dispatch({ type: 'poll' }); }, this.props.pollInterval); }; ... componentDidUpdate(prevProps, prevState) { ... if (this.props.pollInterval) { if ( prevState.isFetching !== this.state.isFetching && !this.state.isFetching ) { this.poll(); } if (prevState.requestId !== this.state.requestId) { this.fetch(); } } } componentWillUnmount() { ... this.tryToClearTimeout(); } ...
Bereaksi kait
const useRequest3 = (url, pollInterval) => { const [state, dispatch] = React.useReducer(reducer, { ... requestId: 1, }); React.useEffect(() => { …(fetch data) }, [state.url, state.requestId]); React.useEffect(() => { if (!pollInterval || state.isFetching) return; const timeoutId = setTimeout(() => { dispatch({ type: "poll" }); }, pollInterval); return () => { clearTimeout(timeoutId); }; }, [pollInterval, state.isFetching]); ... }
Prop baru telah muncul - pollInterval. Setelah menyelesaikan permintaan sebelumnya melalui setTimeout, kami menambah requestId. Dengan kait, kami memiliki useEffect lain, yang kami sebut setTimeout. Dan useEffect lama kami, yang mengirim permintaan, mulai memonitor variabel lain - requestId, yang memberi tahu kami bahwa setTimeout berfungsi, dan sekarang saatnya untuk mengirim permintaan untuk avatar baru.
Dalam Render Prop, saya harus menulis:
- membandingkan nilai requestId dan isFetching sebelumnya dan baru
- hapus timeoutId di dua tempat
- tambahkan properti timeoutId ke kelas
React Hooks memungkinkan Anda untuk menulis secara singkat dan jelas apa yang kami gunakan untuk menjelaskan secara lebih rinci dan tidak selalu jelas.
4) Apa selanjutnya?
Kami dapat terus memperluas fungsionalitas utilitas kami: menerima konfigurasi parameter kueri yang berbeda, data caching, mengonversi respons dan kesalahan, memperbarui data secara paksa dengan parameter yang sama - operasi rutin dalam aplikasi web besar apa pun. Pada proyek kami, kami telah lama mengambil ini menjadi Komponen (perhatian!) Yang terpisah. Ya, karena itu adalah Prop Render. Tetapi dengan rilis Hooks, kami menulis ulang fungsi (useAxiosRequest) dan bahkan menemukan beberapa bug dalam implementasi lama. Anda dapat melihat dan mencoba di sini .