Skip to content

¿Por qué deberías usar React Query o SWR?

La mayoría de nosotros aquí en React-land estamos desarrollando nuestras aplicaciones con la nueva y brillante API Hooks y enviando peticiones a APIs externas como nadie. Quienes somos nuevos con los hooks, puede que hayamos empezado a crearlos como el siguiente ejemplo simplificado:

export const useSearch = () => {
    const [query, setQuery] = useState();
    const [results, setResults] = useState();

    useEffect(() => {
        if (!query)
            return;

        api.search(query)
            .then(results => setResults(results));
    }, [query]);

    return {
        query,
        setQuery,
        results
    }
}

El Problema

Sin embargo, cada vez que se invoca a un hook, esa instancia es única para el componente desde el que se llamó, por lo que podemos encontrar algunos problemas:

  1. Actualizar la consulta anterior en una instancia no la actualizará para las demás.
  2. Si tenemos tres componentes que utilizan un hook que realiza una petición a un API, obtendremos como resultado al menos una solicitud por cada componente.
  3. Si tenemos varias peticiones en el aire, e intentamos almacenarlas en un store global o si el hook mantiene el estado, terminaremos con un estado fuera de sincronización o peticiones posteriores sobrescribiéndose entre sí.

Una forma de resolver esto es dejar la petición fuera de los hooks y solo ejecutarla en un componente que puede garantizar que será una instancia única(también conocido como un componente singleton, como una página/ruta tal vez). Dependiendo de cómo se usen esos datos, esto a veces puede ser complejo de manejar.

Posibles Soluciones

Entonces, ¿qué podemos hacer? He aquí algunas opciones:

  1. Asegúrate de que los datos que provienen de tu API vayan en contexto o por medio de algún tipo de manejo de estado global, aceptando las múltiples peticiones (potencialmente sobrecargando la API de nuestro servidor)
  2. Al hacer lo anterior + usando una librería como react-singleton-hook, asegúrate de que solo haya un componente con useEffect haciendo la llamada al API, o similar para evitar múltiples peticiones.
  3. Implementar algún tipo de caché de datos (mientras sea posible invalidarlo según sea necesario) para que podamos extraer los datos primero desde esa caché.
  4. Usar React Query o SWR

La Solución Real

La solución real aquí es usar la opción 4. Ambos paquetes han implementado una solución sofisticada para resolver estos problemas y evitar que tengas que hacerlo por ti mismo. El almacenamiento en caché es complejo de implementarlo "bien" y podría tener consecuencias inesperadas si se hace mal, que podría derivar en una aplicación "rota" parcial o totalmente.

Otros Problemas Resueltos

A continuación, se muestran algunos ejemplos de otros problemas que estos paquetes pueden resolver. Se muestran ejemplos de código para cada uno(se usa React Query pero es similar a SWR).

Recuperación de Foco en Ventana(Window Focus Refetching)

Un gran problema que encontramos usualmente con sitios y aplicaciones grandes con JavaScript, es que un usuario puede estar en una pestaña o ventana del browser manipulando datos y luego cambiar a otra de la misma aplicación. El problema aquí es que si no mantenemos nuestros datos actualizados, estos pueden no estar sincronizados. Ambos paquetes resuelven esto volviendo a obtener datos una vez que la ventana tiene el foco activo nuevamente. Si no necesitas este comportamiento, simplemente puedes deshabilitarlo como opción.

    const { data: syncedData } = useQuery(id, id => getSyncedData(id), {
        refetchOnWindowFocus: true /* No necesita especificarse, se activa por defecto */
    })

Reintentar Peticiones, Revalidación y Polling

A veces una petición falla temporalmente, sucede. Ambos paquetes resuelven este problema permitiendo la configuración de reintentos automáticos, por lo que cada vez que detecten un error, reintentarán la cantidad de veces especificada hasta que finalmente retorne un error. Además, puedes usar cualquiera de las dos opciones para hacer un poll de un endpoint de manera constante simplemente estableciendo un intervalo de milisegundos para un refetch/refresh.

Ejemplo de Reintento(Retry)

    const { data: books } = useQuery(id, id => getBooks(id), {
        retry: 5, //intentar 5 veces antes de fallar nuevamente
        retryDelay: 1000 //intentar cada segundo
    })

    /* valores por defecto: {
        retry: 3,
        retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000)
    }*/

Ejemplo de Polling

    const indexing = useRef(false);
    const { data: searchResults } = useQuery(['search', keyword], (key, keyword) => search(keyword), {
        //obtener datos cada segundo hasta que se haya finalizado el "indexing"
        refetchInterval: indexing.current ? 1000 : undefined,
        //invalidar la caché de consultas después que esta sea exitosa,
        //hasta que se deje de indexar
        onSuccess: async data => {
            if (data.indexing) {
                await queryCache.invalidateQueries(keyword)
            }
        }
    });

    //actualizar nuestra referencia 
    indexing.current = !!searchResults?.indexing;

Mutación con Actualizaciones Optimistas

Supongamos que tienes una lista de usuarios y deseas actualizar la información de uno de ellos, una operación bastante habitual. La mayoría de los usuarios estarán contentos al ver un indicador de progreso cuando el servidor esté trabajando para actualizar a dicho usuario y esperarán a que termine antes de ver la lista actualizada.

Sin embargo, si sabemos cómo se verá la lista de usuarios actualizada localmente (porque el usuario acaba de realizar dicha acción), ¿Realmente necesitamos mostrar un cargador? No, ambos paquetes permiten hacer mutaciones(cambios) en los datos guardados en caché que se actualizarán de inmediato localmente e iniciarán la actualización en el servidor en segundo plano. También se asegurarán de que los datos se vuelvan a recuperar/validar para que sean los mismos una vez se obtenga la respuesta del servidor, y si no, los datos devueltos estarán en su lugar.

Imagina que tenemos una página donde el usuario puede editar su información. Primero, necesitamos obtener los datos del backend.

    const cache = useQueryCache()
    const userCacheKey = ['user', id];
    const { data: user } = useQuery(userCacheKey, (key, id) => {
        return fetch(`/user/${id}`).then(res => res.json());
    });

A continuación, necesitamos configurar una función que permita actualizar los datos del usuario una vez que estos se envíen desde el formulario.

    const [updateUser] = useMutation(
        newUser => fetch(`/user/${id}`, {
            method: 'POST',
            body: JSON.stringify(newUser)
        }).then(res => res.json()),
        ...

Si queremos que nuestros datos se mantengan actualizados con la interfaz de usuario(UI) de manera optimista, se tiene que agregar algunas opciones para dicha mutación. El atributo onMutate establecerá los datos localmente en el caché antes de la actualización real para que la interfaz de usuario no muestre un indicador de progreso. El valor de retorno se usa en caso de error y necesitaremos restablecer al estado anterior.

    onMutate: newUser => {
        cache.cancelQueries(userCacheKey)

        const oldData = cache.getQueryData(userCacheKey)
        cache.setQueryData(userCacheKey, newUser)

        return oldData
    }

Si estamos actualizando de manera optimista, debemos poder manejar los posibles errores y también asegurarnos de que el servidor devuelva los datos esperados. Entonces es necesario agregar dos hooks más a las opciones de mutación. onError usará los datos devueltos por onMutate para que sea factible restablecer el estado anterior. En cambio, onSettled asegura que se vaya a obtener los mismos datos del servidor para que todo esté sincronizado.

    //Reestablece los datos previos cuando surge un error
    onError: oldUser => {
        cache.setQueryData(userCacheKey, oldUser)
    },
    onSettled: () => {
        cache.invalidateQueries(userCacheKey)
    }

Prefetching y Fetching en Segundo Plano

Si se tiene una idea sobre algunos datos que el usuario podría necesitar, se puede usar estos paquetes para obtener esos datos con antelación(prefetch). Para cuando el usuario llega, los datos ya están listos, lo que hace que la transición sea instantánea. Esta implementación puede hacer que tu aplicación se sienta más ligera.

    const prefetchUpcomingStep = async (stepId) => {
        await cache.prefetchQuery(stepId, stepId => fetch(`/step/${stepId}`))
    }

    //más tarde...

    prefetchUpcomingStep('step-137')
    //esto permite obtener los datos antes de llegar a la consulta misma

Como nota adicional, si el usuario ya ha recibido datos, pero es hora de actualizar, los paquetes buscarán obtenerlos en segundo plano y reemplazarán los datos antiguos si y solo si son diferentes. Esto evita mostrar al usuario un indicador de progreso, notificando únicamente si hay algo nuevo, lo que mejora la experiencia de usuario.

Imagina que tenemos un componente que lista novedades al estilo de Twitter y que constantemente está recibiendo nuevas publicaciones.

    const Feed = () => {
        const { data: feed, isLoading, isFetching } = useQuery(id, id => getFeed(id), {
            refetchInterval: 15000
        });

        ...

Podemos notificar a los usuarios que los datos se están actualizando en segundo plano al "escuchar" que isFetching es true, que se activará incluso si hay datos de caché.


    <header>
        <h1>Your feed</h1>
        {
            isFetching && 
            <Notification>
                <Spinner /> loading new posts
            </Notification>
        }
    </header>

Si no se tiene ningún dato en la caché y la consulta está obteniendo datos, podemos escuchar por isLoading como true y mostrar algún tipo de indicador de progreso. Finalmente, si isSuccess es true y recibimos datos, podemos mostrar las publicaciones.

    <FeedContainer>
        {   
            isLoading && <LoadingCard /> 
        }
        {
            feed && isSuccess && feed.posts.map(post => (
                <Post {...post} />
            ))
        }
    </FeedContainer>

Comparación

El autor de React Query hizo un gran trabajo al crear una tabla de comparación para React Query, SWR y Apollo para que puedas ver qué funciones están disponibles. Una gran característica que me gustaría mencionar de React Query sobre SWR es su propio conjunto de herramientas de desarrollo que son realmente útiles para depurar consultas que puedan estar fallando.

Conclusión

Durante mi tiempo como desarrollador, he intentado resolver estos problemas por mí mismo, y si hubiera tenido un paquete como React Query o SWR, habría ahorrado mucho tiempo. Estos problemas pueden ser realmente un reto a resolver y una solución propia puede terminar inyectando errores sutiles en tu aplicación, que pueden ser difíciles de depurar o que requieren mucho tiempo. Afortunadamente, tenemos código abierto(open source) y estas personas fueron generosas al ofrecer sus soluciones robustas para nosotros.

Si te gustaría conocer más sobre los problemas que resuelven estos paquetes y el esfuerzo necesario para resolverlos, Tanner Linsley hizo un gran relato por lo que estuvo experimentando y cómo lo resolvió. Puedes ver su tutorial aquí:

En general, considero que estos paquetes son excelentes adiciones al ecosistema de desarrollo y nos ayudan a escribir mejor software. Me gustaría ver otros frameworks con opciones similares, porque los conceptos que se mencionan aquí son bastante habituales. Espero que esto te haya resultado útil y nos hagas saber alguna estrategia tuya al usar dichas opciones.

PD. ¿Qué hay de GraphQL? 😂

Bueno, muchas de los paquetes GraphQL que existen en realidad incorporaron estos conceptos desde el principio, por lo que si estás usando algo como Apollo o Urql, muy probablemente ya estés obteniendo estas ventajas. Sin embargo, ambas librerías son compatibles con cualquier función que retorne una Promesa, por lo que si tu librería GQL favorita no tiene estas características, intenta usar React Query o SWR. 😁

Este artículo es una traducción al español de su versión en inglés