Permintaan API dengan React Hooks, HOC, atau Render Prop


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:


RPropHoc
1. Banyak komponen pembungkus yang sulit dipahami dalam Bereaksi DevTools dan dalam kode.(◕︵◕)(◕︵◕)
2. Sulit untuk mengetik (Flow, TypeScript).(◕︵◕)
3. Tidak jelas dari HOC mana alat peraga menerima komponen, yang mempersulit proses debugging dan memahami bagaimana komponen bekerja.(◕︵◕)
4. Render Prop paling sering tidak menambahkan tata letak, meskipun digunakan secara internal oleh BEJ.(◕︵◕)
5. Alat peraga tabrakan kunci. Saat mengirimkan alat peraga dari orang tua, kunci yang sama dapat ditimpa dengan nilai-nilai dari HOC.(◕︵◕)
6. Sulit membaca git diff, karena semua lekukan di JSX bergeser saat membungkus JSX di Render Prop.(◕︵◕)
7. Jika ada beberapa HOC, maka Anda dapat membuat kesalahan dengan urutan komposisi. Urutan yang benar tidak selalu jelas, karena logika disembunyikan di dalam HOC. Misalnya, ketika kami pertama kali memeriksa apakah pengguna diotorisasi, dan hanya dengan itu kami meminta data pribadi.(◕︵◕)

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:


KaitRProp
1. Lebih sedikit kode.(◑‿◐)
2. Memanggil efek samping (meminta data dalam API) lebih mudah dibaca, karena ditulis secara linear, tidak tersebar di seluruh siklus hidup komponen.(◑‿◐)
3. Pembatalan permintaan ditulis segera setelah permintaan dipanggil. Semua ada di satu tempat.(◑‿◐)
4. Kode sederhana yang menjelaskan parameter pelacakan untuk memicu efek samping.(◑‿◐)
5. Jelas, dalam siklus hidup komponen apa kode kita akan dieksekusi.(◑‿◐)

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 baru

Memahami 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:


  1. membandingkan nilai requestId dan isFetching sebelumnya dan baru
  2. hapus timeoutId di dua tempat
  3. 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 .

Source: https://habr.com/ru/post/id453866/


All Articles