Cómo crear un gancho de reacción personalizado para obtener y almacenar datos en caché
Publicado: 2022-03-10componentDidMount()
, pero con la introducción de Hooks, puede crear un hook personalizado que obtendrá y almacenará los datos por usted. Eso es lo que cubrirá este tutorial.Si es un novato en React Hooks, puede comenzar consultando la documentación oficial para comprenderlo. Después de eso, recomendaría leer "Primeros pasos con la API de React Hooks" de Shedrack Akintayo. Para asegurarse de que está siguiendo, también hay un artículo escrito por Adeneye David Abiodun que cubre las mejores prácticas con React Hooks que estoy seguro le resultará útil.
A lo largo de este artículo, utilizaremos la API de búsqueda de Hacker News para crear un gancho personalizado que podamos usar para obtener datos. Si bien este tutorial cubrirá la API de búsqueda de Hacker News, haremos que el enlace funcione de manera que devuelva la respuesta de cualquier enlace de API válido que le pasemos.
Mejores prácticas de reacción
React es una fantástica biblioteca de JavaScript para crear interfaces de usuario ricas. Proporciona una excelente abstracción de componentes para organizar sus interfaces en un código que funcione bien, y puede usarlo para casi cualquier cosa. Lea un artículo relacionado en React →
Obtener datos en un componente React
Antes de los ganchos de React, era convencional obtener datos iniciales en el método de ciclo de vida de componentDidMount()
y datos basados en cambios de prop o estado en el método de ciclo de vida de componentDidUpdate()
.
Así es como funciona:
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(); } }
El método del ciclo de vida del componentDidMount
se invoca tan pronto como se monta el componente, y cuando se hace eso, lo que hicimos fue realizar una solicitud para buscar "JavaScript" a través de la API de Hacker News y actualizar el estado según la respuesta.
El método de ciclo de vida componentDidUpdate
, por otro lado, se invoca cuando hay un cambio en el componente. Comparamos la consulta anterior en el estado con la consulta actual para evitar que se invoque el método cada vez que configuramos "datos" en el estado. Una cosa que obtenemos al usar ganchos es combinar ambos métodos de ciclo de vida de una manera más limpia, lo que significa que no necesitaremos tener dos métodos de ciclo de vida para cuando el componente se monta y cuando se actualiza.
Obtener datos con useEffect
Hook
El gancho useEffect
se invoca tan pronto como se monta el componente. Si necesitamos que el enlace se vuelva a ejecutar en función de algunos cambios de propiedad o estado, necesitaremos pasarlos a la matriz de dependencia (que es el segundo argumento del useEffect
).
Exploremos cómo obtener datos con ganchos:
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]);
En el ejemplo anterior, pasamos la query
como una dependencia a nuestro useEffect
. Al hacer eso, le estamos diciendo a useEffect
que realice un seguimiento de los cambios de consulta. Si el valor de query
anterior no es el mismo que el valor actual, useEffect
se vuelve a invocar.
Dicho esto, también estamos configurando varios status
en el componente según sea necesario, ya que esto transmitirá mejor algún mensaje a la pantalla en función del status
de algunos estados finitos. En el estado inactivo , podemos informar a los usuarios que pueden usar el cuadro de búsqueda para comenzar. En el estado de búsqueda , podríamos mostrar una rueda giratoria . Y, en el estado obtenido , representaremos los datos.
Es importante configurar los datos antes de intentar establecer el estado en fetched
para que pueda evitar un parpadeo que se produce como resultado de que los datos estén vacíos mientras configura el estado fetched
.
Crear un gancho personalizado
"Un enlace personalizado es una función de JavaScript cuyo nombre comienza con 'uso' y que puede llamar a otros enlaces".
— Reaccionar Documentos
Eso es realmente lo que es, y junto con una función de JavaScript, le permite reutilizar una parte del código en varias partes de su aplicación.
La definición de React Docs lo ha delatado, pero veamos cómo funciona en la práctica con un gancho personalizado de contador:
const useCounter = (initialState = 0) => { const [count, setCount] = useState(initialState); const add = () => setCount(count + 1); const subtract = () => setCount(count - 1); return { count, add, subtract }; };
Aquí, tenemos una función regular en la que tomamos un argumento opcional, establecemos el valor en nuestro estado, así como agregamos los métodos de add
y subtract
que podrían usarse para actualizarlo.
En cualquier parte de nuestra aplicación donde necesitemos un contador, podemos llamar a useCounter
como una función normal y pasar un initialState
para que sepamos desde dónde empezar a contar. Cuando no tenemos un estado inicial, por defecto es 0.
Así es como funciona en la práctica:
import { useCounter } from './customHookPath'; const { count, add, subtract } = useCounter(100); eventHandler(() => { add(); // or subtract(); });
Lo que hicimos aquí fue importar nuestro enlace personalizado desde el archivo en el que lo declaramos, para poder usarlo en nuestra aplicación. Establecemos su estado inicial en 100, por lo que cada vez que llamamos a add()
, aumenta la count
en 1, y cada vez que llamamos a subtract()
, disminuye la count
en 1.
Creando useFetch
Hook
Ahora que hemos aprendido cómo crear un enlace personalizado simple, extraigamos nuestra lógica para obtener datos en un enlace personalizado.
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 }; };
Es más o menos lo mismo que hicimos anteriormente, con la excepción de que es una función que recibe una query
y devuelve status
y data
. Y ese es un useFetch
que podríamos usar en varios componentes en nuestra aplicación React.

Esto funciona, pero el problema con esta implementación ahora es que es específico de Hacker News, por lo que podríamos llamarlo useHackerNews
. Lo que pretendemos hacer es crear un useFetch
que se pueda usar para llamar a cualquier URL. ¡Vamos a renovarlo para que admita una URL en su lugar!
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 }; };
Ahora, nuestro enlace useFetch es genérico y podemos usarlo como queramos en nuestros diversos componentes.
Aquí hay una forma de consumirlo:
const [query, setQuery] = useState(''); const url = query && `https://hn.algolia.com/api/v1/search?query=${query}`; const { status, data } = useFetch(url);
En este caso, si el valor de query
es truthy
, seguimos adelante para establecer la URL y, si no lo es, estamos de acuerdo con pasar undefined ya que se manejaría en nuestro enlace. El efecto intentará ejecutarse una vez, independientemente.
Memorización de datos obtenidos
La memorización es una técnica que usaríamos para asegurarnos de no llegar al punto final de las noticias de hackernews
si hemos realizado algún tipo de solicitud para obtenerlo en alguna fase inicial. Almacenar el resultado de costosas llamadas de recuperación ahorrará a los usuarios algo de tiempo de carga y, por lo tanto, aumentará el rendimiento general.
Nota : para obtener más contexto, puede consultar la explicación de Wikipedia sobre Memoización.
¡Exploremos cómo podríamos hacer eso!
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 }; };
Aquí, estamos asignando URL a sus datos. Entonces, si hacemos una solicitud para obtener algunos datos existentes, configuramos los datos de nuestro caché local, de lo contrario, hacemos la solicitud y configuramos el resultado en el caché. Esto garantiza que no hagamos una llamada a la API cuando tengamos los datos disponibles localmente. También notaremos que estamos eliminando el efecto si la URL es falsy
, por lo que se asegura de que no procedamos a obtener datos que no existen. No podemos hacerlo antes del gancho useEffect
ya que eso irá en contra de una de las reglas de los ganchos, que es siempre llamar a los ganchos en el nivel superior.
Declarar cache
en un ámbito diferente funciona, pero hace que nuestro gancho vaya en contra del principio de una función pura. Además, también queremos asegurarnos de que React ayude a limpiar nuestro desorden cuando ya no queramos usar el componente. Exploraremos useRef
para ayudarnos a lograrlo.
Memorización de datos con useRef
"useRef
es como un cuadro que puede contener un valor mutable en su.current property
".
— Reaccionar Documentos
Con useRef
, podemos establecer y recuperar valores mutables fácilmente y su valor persiste durante todo el ciclo de vida del componente.
¡Reemplacemos nuestra implementación de caché con algo de 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 }; };
Aquí, nuestro caché está ahora en nuestro gancho useFetch
con un objeto vacío como valor inicial.
Terminando
Bueno, dije que configurar los datos antes de configurar el estado obtenido fue una buena idea, pero también hay dos problemas potenciales que podríamos tener con eso:
- Nuestra prueba unitaria podría fallar como resultado de que la matriz de datos no esté vacía mientras estamos en el estado de recuperación. React podría en realidad hacer cambios de estado por lotes, pero no puede hacerlo si se activa de forma asíncrona;
- Nuestra aplicación vuelve a renderizar más de lo que debería.
Hagamos una limpieza final de nuestro useFetch
. Vamos a comenzar cambiando nuestro useState
s a useReducer
. ¡Veamos cómo funciona!
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);
Aquí, agregamos un estado inicial que es el valor inicial que pasamos a cada uno de nuestros useState
individuales. En nuestro useReducer
, verificamos qué tipo de acción queremos realizar y establecemos los valores apropiados para indicar en función de eso.
Esto resuelve los dos problemas que discutimos anteriormente, ya que ahora podemos configurar el estado y los datos al mismo tiempo para ayudar a evitar estados imposibles y re-renderizaciones innecesarias.
Solo queda una cosa más: limpiar nuestro efecto secundario. Fetch implementa la API de Promise, en el sentido de que podría resolverse o rechazarse. Si nuestro enlace intenta realizar una actualización mientras el componente se ha desmontado debido a que se acaba de resolver alguna Promise
, React devolverá Can't perform a React state update on an unmounted component.
¡Veamos cómo podemos arreglar eso con la limpieza 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]);
Aquí, establecemos cancelRequest
en true
después de haberlo definido dentro del efecto. Entonces, antes de intentar realizar cambios de estado, primero confirmamos si el componente se ha desmontado. Si se ha desmontado, omitimos actualizar el estado y si no se ha desmontado, actualizamos el estado. Esto resolverá el error de actualización del estado de React y también evitará condiciones de carrera en nuestros componentes.
Conclusión
Hemos explorado varios conceptos de enlaces para ayudar a obtener y almacenar datos en caché en nuestros componentes. También pasamos por la limpieza de nuestro gancho useEffect
que ayuda a prevenir una buena cantidad de problemas en nuestra aplicación.
Si tiene alguna pregunta, ¡no dude en dejarla en la sección de comentarios a continuación!
- Ver el repositorio de este artículo →
Referencias
- "Presentamos ganchos", React Docs
- “Primeros pasos con la API de React Hooks”, Shedrack Akintayo
- “Mejores prácticas con ganchos de reacción”, Adeneye David Abiodun
- “Programación funcional: funciones puras”, Arne Brasseur