Come creare un hook di reazione personalizzato per recuperare e memorizzare nella cache i dati

Pubblicato: 2022-03-10
Riepilogo rapido ↬ C'è un'alta possibilità che molti componenti nella tua applicazione React debbano effettuare chiamate a un'API per recuperare i dati che verranno mostrati ai tuoi utenti. È già possibile farlo utilizzando il metodo del ciclo di vita componentDidMount() , ma con l'introduzione di Hooks, puoi creare un hook personalizzato che recupererà e memorizzerà i dati nella cache per te. Questo è ciò che tratterà questo tutorial.

Se sei un principiante di React Hooks, puoi iniziare controllando la documentazione ufficiale per averne un'idea. Dopodiché, consiglierei di leggere "Getting Started With React Hooks API" di Shedrack Akintayo. Per assicurarti di seguirti, c'è anche un articolo scritto da Adeneye David Abiodun che copre le migliori pratiche con React Hooks che sono sicuro si rivelerà utile per te.

In questo articolo, utilizzeremo l'API Hacker News Search per creare un hook personalizzato che possiamo utilizzare per recuperare i dati. Sebbene questo tutorial tratterà l'API Hacker News Search, faremo in modo che l'hook funzioni in modo da restituire una risposta da qualsiasi collegamento API valido che gli passiamo.

Migliori pratiche di reazione

React è una fantastica libreria JavaScript per la creazione di interfacce utente avanzate. Fornisce un'ottima astrazione dei componenti per organizzare le tue interfacce in codice ben funzionante e c'è praticamente qualsiasi cosa per cui puoi usarlo. Leggi un articolo correlato su Reagire →

Recupero dei dati in un componente React

Prima degli hook di React, era convenzionale recuperare i dati iniziali nel metodo del ciclo di vita componentDidMount() e i dati basati su prop o cambiamenti di stato nel metodo del ciclo di vita componentDidUpdate() .

Ecco come funziona:

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

Il metodo del ciclo di vita componentDidMount viene invocato non appena il componente viene montato e, una volta fatto, ciò che abbiamo fatto è stato fare una richiesta per cercare "JavaScript" tramite l'API Hacker News e aggiornare lo stato in base alla risposta.

Il metodo del ciclo di vita componentDidUpdate , d'altra parte, viene richiamato quando si verifica una modifica nel componente. Abbiamo confrontato la query precedente nello stato con la query corrente per impedire che il metodo venga invocato ogni volta che impostiamo "dati" nello stato. Una cosa che otteniamo usando gli hook è combinare entrambi i metodi del ciclo di vita in un modo più pulito, il che significa che non avremo bisogno di avere due metodi del ciclo di vita per quando il componente si monta e quando si aggiorna.

Altro dopo il salto! Continua a leggere sotto ↓

Recupero dei dati con useEffect Hook

L'hook useEffect viene invocato non appena il componente viene montato. Se abbiamo bisogno che l'hook venga eseguito nuovamente in base ad alcune modifiche di stato o prop, dovremo passarle all'array di dipendenza (che è il secondo argomento useEffect ).

Esploriamo come recuperare i dati con gli hook:

 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]);

Nell'esempio sopra, abbiamo passato la query come dipendenza al nostro hook useEffect . In questo modo, stiamo dicendo a useEffect di tenere traccia delle modifiche alle query. Se il valore della query precedente non è uguale al valore corrente, useEffect viene richiamato di nuovo.

Detto questo, stiamo anche impostando diversi stati sul componente secondo necessità, in quanto ciò trasmetterà meglio alcuni messaggi status schermo in base ad alcuni stati status . Nello stato inattivo , potremmo far sapere agli utenti che possono utilizzare la casella di ricerca per iniziare. Nello stato di recupero , potremmo mostrare uno spinner . E, nello stato recuperato , eseguiremo il rendering dei dati.

È importante impostare i dati prima di tentare di impostare lo stato su fetched in modo da evitare uno sfarfallio dovuto al fatto che i dati sono vuoti durante l'impostazione dello stato fetched .

Creazione di un gancio personalizzato

"Un hook personalizzato è una funzione JavaScript il cui nome inizia con 'use' e che può chiamare altri hook."

— Reagisci documenti

Questo è davvero quello che è e, insieme a una funzione JavaScript, ti consente di riutilizzare alcuni pezzi di codice in diverse parti della tua app.

La definizione da React Docs l'ha tradita ma vediamo come funziona in pratica con un hook counter custom:

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

Qui, abbiamo una funzione regolare in cui prendiamo un argomento facoltativo, impostiamo il valore sul nostro stato, nonché aggiungiamo i metodi di add e subtract che potrebbero essere utilizzati per aggiornarlo.

Ovunque nella nostra app dove abbiamo bisogno di un contatore, possiamo chiamare useCounter come una normale funzione e passare un initialState in modo da sapere da dove iniziare a contare. Quando non abbiamo uno stato iniziale, il valore predefinito è 0.

Ecco come funziona in pratica:

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

Quello che abbiamo fatto qui è stato importare il nostro hook personalizzato dal file in cui lo abbiamo dichiarato, in modo da poterlo utilizzare nella nostra app. Impostiamo il suo stato iniziale su 100, quindi ogni volta che chiamiamo add() , aumenta il count di 1 e ogni volta che chiamiamo subtract() , diminuisce il count di 1.

Creazione useFetch Hook

Ora che abbiamo imparato come creare un semplice hook personalizzato, estraiamo la nostra logica per recuperare i dati in un hook personalizzato.

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

È più o meno la stessa cosa che abbiamo fatto sopra, con l'eccezione del fatto che è una funzione che accetta query e restituisce status e data . E questo è un hook useFetch che potremmo usare in diversi componenti nella nostra applicazione React.

Funziona, ma il problema con questa implementazione ora è che è specifico di Hacker News, quindi potremmo semplicemente chiamarlo useHackerNews . Quello che intendiamo fare è creare un hook useFetch che può essere utilizzato per chiamare qualsiasi URL. Rinnoviamolo per inserire invece un 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 }; };

Ora, il nostro hook useFetch è generico e possiamo usarlo come vogliamo nei nostri vari componenti.

Ecco un modo per consumarlo:

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

In questo caso, se il valore di query è truthy , andiamo avanti per impostare l'URL e, in caso contrario, possiamo passare undefined poiché verrebbe gestito nel nostro hook. L'effetto tenterà di essere eseguito una volta, a prescindere.

Memorizzazione dei dati recuperati

La memorizzazione è una tecnica che useremmo per assicurarci di non raggiungere l'endpoint di hackernews se avessimo fatto una sorta di richiesta per recuperarlo in una fase iniziale. La memorizzazione del risultato di costose chiamate di recupero farà risparmiare agli utenti un po' di tempo di caricamento, aumentando quindi le prestazioni complessive.

Nota : per più contesto, puoi consultare la spiegazione di Wikipedia sulla memorizzazione.

Esploriamo come potremmo farlo!

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

Qui stiamo mappando gli URL ai loro dati. Quindi, se facciamo una richiesta per recuperare alcuni dati esistenti, impostiamo i dati dalla nostra cache locale, altrimenti andiamo avanti per effettuare la richiesta e impostare il risultato nella cache. Ciò garantisce che non effettuiamo una chiamata API quando abbiamo i dati disponibili localmente. Noteremo anche che stiamo eliminando l'effetto se l'URL è falsy , quindi assicurati di non procedere al recupero di dati che non esistono. Non possiamo farlo prima useEffect in quanto andrà contro una delle regole degli hook, che è chiamare sempre hook al livello superiore.

Dichiarare la cache in un ambito diverso funziona ma fa sì che il nostro hook vada contro il principio di una pura funzione. Inoltre, vogliamo anche assicurarci che React aiuti a ripulire il nostro pasticcio quando non vogliamo più utilizzare il componente. Esploreremo useRef per aiutarci a raggiungere questo obiettivo.

Memorizzazione dei dati con useRef

" useRef è come una scatola che può contenere un valore mutabile nella sua .current property ."

— Reagisci documenti

Con useRef , possiamo impostare e recuperare valori modificabili a nostro agio e il suo valore persiste per tutto il ciclo di vita del componente.

Sostituiamo la nostra implementazione della cache con un po' di magia 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 }; };

Qui, la nostra cache è ora nel nostro hook useFetch con un oggetto vuoto come valore iniziale.

Avvolgendo

Bene, ho affermato che impostare i dati prima di impostare lo stato recuperato era una buona idea, ma ci sono due potenziali problemi che potremmo avere anche con quello:

  1. Il nostro unit test potrebbe non riuscire perché l'array di dati non è vuoto mentre siamo nello stato di recupero. React potrebbe effettivamente modificare lo stato in batch ma non può farlo se viene attivato in modo asincrono;
  2. La nostra app esegue nuovamente il rendering di più di quanto dovrebbe.

Facciamo una pulizia finale del nostro hook useFetch .,Inizieremo cambiando il nostro useState s in un useReducer . Vediamo come funziona!

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

Qui, abbiamo aggiunto uno stato iniziale che è il valore iniziale che abbiamo passato a ciascuno dei nostri useState individuali. Nel nostro useReducer , controlliamo quale tipo di azione vogliamo eseguire e impostiamo i valori appropriati da dichiarare in base a quello.

Questo risolve i due problemi di cui abbiamo discusso in precedenza, poiché ora possiamo impostare lo stato e i dati contemporaneamente per aiutare a prevenire stati impossibili e ripetizioni non necessarie.

Rimane solo un'altra cosa: ripulire il nostro effetto collaterale. Fetch implementa l'API Promise, nel senso che potrebbe essere risolta o rifiutata. Se il nostro hook tenta di eseguire un aggiornamento mentre il componente è smontato a causa di alcune Promise appena risolte, React restituisce Can't perform a React state update on an unmounted component.

Vediamo come possiamo risolverlo con la pulizia useEffect !

 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]);

Qui, impostiamo cancelRequest su true dopo averlo definito all'interno dell'effetto. Quindi, prima di tentare di apportare modifiche allo stato, confermiamo innanzitutto se il componente è stato smontato. Se è stato smontato, saltiamo l'aggiornamento dello stato e se non è stato smontato, aggiorniamo lo stato. Ciò risolverà l'errore di aggiornamento dello stato di React e preverrà anche le condizioni di gara nei nostri componenti.

Conclusione

Abbiamo esplorato diversi concetti di hook per aiutare a recuperare e memorizzare nella cache i dati nei nostri componenti. Abbiamo anche ripulito il nostro hook useEffect che aiuta a prevenire un buon numero di problemi nella nostra app.

Se hai domande, non esitare a lasciarle nella sezione commenti qui sotto!

  • Vedi il repository per questo articolo →

Riferimenti

  • "Presentazione di Hooks", React Docs
  • "Introduzione all'API React Hooks", Shedrack Akintayo
  • "Migliori pratiche con React Hooks", Adeneye David Abiodun
  • "Programmazione funzionale: funzioni pure", Arne Brasseur