Cum să creați un cârlig personalizat de reacție pentru a prelua și stoca datele în cache

Publicat: 2022-03-10
Rezumat rapid ↬ Există o mare posibilitate ca multe componente din aplicația dvs. React să fie nevoite să apeleze la un API pentru a prelua date care vor fi afișate utilizatorilor dvs. Este deja posibil să faceți asta folosind metoda ciclului de viață componentDidMount() , dar odată cu introducerea Hooks, puteți construi un hook personalizat care va prelua și va stoca în cache datele pentru dvs. Asta va acoperi acest tutorial.

Dacă sunteți începător la React Hooks, puteți începe prin a verifica documentația oficială pentru a înțelege. După aceea, aș recomanda să citești „Noțiuni introductive cu API-ul React Hooks” al lui Shedrack Akintayo. Pentru a vă asigura că urmați, există și un articol scris de Adeneye David Abiodun care acoperă cele mai bune practici cu React Hooks, care sunt sigur că se va dovedi a vă fi util.

Pe parcursul acestui articol, vom folosi API-ul Hacker News Search pentru a construi un cârlig personalizat pe care îl putem folosi pentru a prelua date. Deși acest tutorial va acoperi API-ul Hacker News Search, vom avea cârligul să funcționeze într-un mod în care va returna răspuns de la orice link API valid pe care îl transmitem.

Cele mai bune practici React

React este o bibliotecă JavaScript fantastică pentru construirea de interfețe bogate pentru utilizator. Oferă o abstractizare excelentă a componentelor pentru organizarea interfețelor în cod care funcționează bine și există aproape orice pentru care îl puteți folosi. Citiți un articol similar pe React →

Preluarea datelor într-o componentă React

Înainte de conectarea React, era obișnuit să preluați datele inițiale în metoda ciclului de viață componentDidMount() și date bazate pe modificarea prop sau a stării în metoda ciclului de viață componentDidUpdate() .

Iată cum funcționează:

 componentDidMount() { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=JavaScript` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } componentDidUpdate(previousProps, previousState) { if (previousState.query !== this.state.query) { const fetchData = async () => { const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${this.state.query}` ); const data = await response.json(); this.setState({ data }); }; fetchData(); } }

Metoda ciclului de viață componentDidMount este invocată de îndată ce componenta este montată și, când s-a terminat, ceea ce am făcut a fost să facem o solicitare de căutare a „JavaScript” prin intermediul API-ului Hacker News și să actualizăm starea pe baza răspunsului.

Metoda ciclului de viață componentDidUpdate , pe de altă parte, este invocată atunci când există o schimbare în componentă. Am comparat interogarea anterioară în stare cu interogarea curentă pentru a preveni invocarea metodei de fiecare dată când setăm „date” în stare. Un lucru pe care îl obținem din utilizarea cârligelor este să combinăm ambele metode de ciclu de viață într-un mod mai curat - ceea ce înseamnă că nu va fi nevoie să avem două metode de ciclu de viață pentru când componenta se montează și când se actualizează.

Mai multe după săritură! Continuați să citiți mai jos ↓

Preluarea datelor cu useEffect Hook

Cârligul useEffect este invocat de îndată ce componenta este montată. Dacă avem nevoie ca hook-ul să fie reluat pe baza unor modificări de prop sau de stare, va trebui să le transmitem matricei de dependențe (care este al doilea argument al hook- useEffect ).

Să explorăm cum să obținem date cu cârlige:

 import { useState, useEffect } from 'react'; const [status, setStatus] = useState('idle'); const [query, setQuery] = useState(''); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]);

În exemplul de mai sus, am transmis query ca dependență la cârligul nostru useEffect . Făcând asta, îi spunem lui useEffect să urmărească modificările interogărilor. Dacă valoarea query anterioare nu este aceeași cu valoarea curentă, useEffect este invocat din nou.

Acestea fiind spuse, setăm, de asemenea, mai multe status pe componentă, după cum este necesar, deoarece acest lucru va transmite mai bine un mesaj pe ecran pe baza unor stări status . În starea inactivă , am putea informa utilizatorii că ar putea folosi caseta de căutare pentru a începe. În starea de preluare , am putea arăta un filator . Și, în starea preluată , vom reda datele.

Este important să setați datele înainte de a încerca să setați starea la fetched , astfel încât să puteți preveni o pâlpâire care apare ca urmare a golirii datelor în timp ce setați starea fetched .

Crearea unui cârlig personalizat

„Un cârlig personalizat este o funcție JavaScript al cărei nume începe cu „utilizare” și care poate apela alte cârlige.”

— React Docs

Asta este cu adevărat și, împreună cu o funcție JavaScript, vă permite să reutilizați o parte de cod în mai multe părți ale aplicației dvs.

Definiția din React Docs l-a dat, dar să vedem cum funcționează în practică cu un cârlig personalizat:

 const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };

Aici, avem o funcție obișnuită în care luăm un argument opțional, setăm valoarea la starea noastră, precum și adăugăm metodele de add și subtract care ar putea fi utilizate pentru a-l actualiza.

Oriunde în aplicația noastră, unde avem nevoie de un contor, putem apela useCounter ca o funcție obișnuită și putem trece un initialState , astfel încât să știm de unde să începem numărarea. Când nu avem o stare inițială, implicit la 0.

Iată cum funcționează în practică:

 import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });

Ceea ce am făcut aici a fost să importam cârligul nostru personalizat din fișierul în care l-am declarat, astfel încât să îl putem folosi în aplicația noastră. Îi setăm starea inițială la 100, așa că, ori de câte ori apelăm add() , crește count cu 1, iar oricând apelăm subtract() , scade count cu 1.

Se creează useFetch Hook

Acum că am învățat cum să creăm un cârlig personalizat simplu, să extragem logica noastră pentru a prelua date într-un cârlig personalizat.

 const useFetch = (query) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!query) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch( `https://hn.algolia.com/api/v1/search?query=${query}` ); const data = await response.json(); setData(data.hits); setStatus('fetched'); }; fetchData(); }, [query]); return { status, data }; };

Este aproape același lucru pe care l-am făcut mai sus, cu excepția faptului că este o funcție care preia query și returnează status și data . Și acesta este un cârlig useFetch pe care l-am putea folosi în mai multe componente din aplicația noastră React.

Acest lucru funcționează, dar problema cu această implementare acum este că este specific Hacker News, așa că am putea numi doar useHackerNews . Ceea ce intenționăm să facem este să creăm un hook useFetch care poate fi folosit pentru a apela orice URL. Să-l renovăm pentru a include o adresă URL!

 const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); const response = await fetch(url); const data = await response.json(); setData(data); setStatus('fetched'); }; fetchData(); }, [url]); return { status, data }; };

Acum, cârligul nostru useFetch este generic și îl putem folosi așa cum dorim în diferitele noastre componente.

Iată o modalitate de a o consuma:

 const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);

În acest caz, dacă valoarea query este truthy , continuăm să setăm adresa URL, iar dacă nu este, suntem bine să trecem undefined, deoarece ar fi gestionat în hook. Efectul va încerca să ruleze o dată, indiferent.

Memorarea datelor preluate

Memorarea este o tehnică pe care am folosi-o pentru a ne asigura că nu ajungem la punctul final de hackernews dacă am solicitat un fel de preluare la o fază inițială. Stocarea rezultatului apelurilor de preluare costisitoare va economisi utilizatorilor ceva timp de încărcare, prin urmare, crește performanța generală.

Notă : pentru mai mult context, puteți consulta explicația Wikipedia despre Memoization.

Să vedem cum am putea face asta!

 const cache = {}; const useFetch = (url) => { const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache[url]) { const data = cache[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Aici mapăm adresele URL cu datele lor. Deci, dacă facem o solicitare de preluare a unor date existente, setăm datele din cache-ul nostru local, altfel, facem cererea și setăm rezultatul în cache. Acest lucru ne asigură că nu efectuăm un apel API atunci când avem datele disponibile la nivel local. Vom observa, de asemenea, că eliminăm efectul dacă adresa URL este falsy , astfel încât să ne asigurăm că nu procedăm la preluarea datelor care nu există. Nu o putem face înainte de useEffect hook, deoarece aceasta va fi împotriva uneia dintre regulile hook-urilor, care este să apelăm întotdeauna hook-uri la nivelul superior.

Declararea cache -ului într-un domeniu diferit funcționează, dar face ca hook-ul nostru să fie împotriva principiului unei funcții pure. În plus, vrem să ne asigurăm că React ne ajută să ne curățăm mizeria atunci când nu mai dorim să folosim componenta. Vom explora useRef pentru a ne ajuta să reușim acest lucru.

Memorarea datelor cu useRef

useRef este ca o casetă care poate deține o valoare mutabilă în .current property .”

— React Docs

Cu useRef , putem seta și prelua cu ușurință valorile modificabile, iar valoarea acesteia persistă pe tot parcursul ciclului de viață al componentei.

Să înlocuim implementarea noastră cache cu ceva magie useRef !

 const useFetch = (url) => { const cache = useRef({}); const [status, setStatus] = useState('idle'); const [data, setData] = useState([]); useEffect(() => { if (!url) return; const fetchData = async () => { setStatus('fetching'); if (cache.current[url]) { const data = cache.current[url]; setData(data); setStatus('fetched'); } else { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; // set response in cache; setData(data); setStatus('fetched'); } }; fetchData(); }, [url]); return { status, data }; };

Aici, cache-ul nostru se află acum în cârligul nostru useFetch cu un obiect gol ca valoare inițială.

Încheierea

Ei bine, am afirmat că setarea datelor înainte de a seta starea preluată a fost o idee bună, dar există două probleme potențiale pe care le-am putea avea și cu asta:

  1. Testul nostru unitar ar putea eșua, deoarece matricea de date nu este goală în timp ce suntem în starea de preluare. React ar putea de fapt să modifice starea loturilor, dar nu poate face asta dacă este declanșat asincron;
  2. Aplicația noastră redă mai mult decât ar trebui.

Să facem o curățare finală a cârligului nostru useFetch .,Vom începe prin a trece useState -urile noastre la un useReducer . Să vedem cum funcționează!

 const initialState = { status: 'idle', error: null, data: [], }; const [state, dispatch] = useReducer((state, action) => { switch (action.type) { case 'FETCHING': return { ...initialState, status: 'fetching' }; case 'FETCHED': return { ...initialState, status: 'fetched', data: action.payload }; case 'FETCH_ERROR': return { ...initialState, status: 'error', error: action.payload }; default: return state; } }, initialState);

Aici, am adăugat o stare inițială care este valoarea inițială pe care am transmis-o fiecăruia dintre useState -urile noastre individuale. În useReducer -ul nostru, verificăm ce tip de acțiune dorim să realizăm și setăm valorile adecvate pe baza acesteia.

Acest lucru rezolvă cele două probleme pe care le-am discutat mai devreme, deoarece acum putem seta starea și datele în același timp pentru a ajuta la prevenirea stărilor imposibile și a redărilor inutile.

Mai rămâne doar un lucru: curățarea efectului nostru secundar. Fetch implementează API-ul Promise, în sensul că ar putea fi rezolvat sau respins. Dacă hook-ul nostru încearcă să facă o actualizare în timp ce componenta s-a demontat din cauza unei Promise care tocmai a fost rezolvată, React va returna Can't perform a React state update on an unmounted component.

Să vedem cum putem remedia asta cu useEffect clean-up!

 useEffect(() => { let cancelRequest = false; if (!url) return; const fetchData = async () => { dispatch({ type: 'FETCHING' }); if (cache.current[url]) { const data = cache.current[url]; dispatch({ type: 'FETCHED', payload: data }); } else { try { const response = await fetch(url); const data = await response.json(); cache.current[url] = data; if (cancelRequest) return; dispatch({ type: 'FETCHED', payload: data }); } catch (error) { if (cancelRequest) return; dispatch({ type: 'FETCH_ERROR', payload: error.message }); } } }; fetchData(); return function cleanup() { cancelRequest = true; }; }, [url]);

Aici, setăm cancelRequest la true după ce l-am definit în interiorul efectului. Deci, înainte de a încerca să facem modificări de stare, mai întâi confirmăm dacă componenta a fost demontată. Dacă a fost demontat, omitem actualizarea stării și dacă nu a fost demontat, actualizăm starea. Acest lucru va rezolva eroarea de actualizare a stării React și, de asemenea, va preveni condițiile de cursă în componentele noastre.

Concluzie

Am explorat câteva concepte de cârlige pentru a ajuta la preluarea și stocarea în cache a datelor din componentele noastre. De asemenea, am curățat cârligul useEffect , care ajută la prevenirea unui număr mare de probleme în aplicația noastră.

Dacă aveți întrebări, nu ezitați să le trimiteți în secțiunea de comentarii de mai jos!

  • Vezi repo pentru acest articol →

Referințe

  • „Prezentarea cârligelor”, React Docs
  • „Noțiuni introductive cu API-ul React Hooks”, Shedrack Akintayo
  • „Cele mai bune practici cu React Hooks”, Adeneye David Abiodun
  • „Programare funcțională: funcții pure”, Arne Brasseur