DEV Community

Cover image for React Query - why does it matter?
Caio Borghi
Caio Borghi

Posted on

React Query - why does it matter?

It avoids useEffect hell and handles: request state management, caching, refetching, retrying, "suspending" and error treatment; out of the box.

It helps with Asynchronous State management.

useEffect hell

You probably don't need useEffect, specially for handling requests.

Code difference

bad

import { useState, useEffect } from 'react';

const UniverseList = () => {
  const [isLoading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [universes, setUniverses] = useState([]);

  useEffect(() => {
    const controller = new AbortController();

    const loadUniverses = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('/api/universes', {
          signal: controller.signal
        });

        if (!response.ok) {
          throw new Error(`Error: ${response.status} - ${await response.text()}`);
        }

        const jsonResponse = await response.json();
        setUniverses(jsonResponse.data || []);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message || 'Failed to load universes');
        }
      } finally {
        setLoading(false);
      }
    };

    loadUniverses();

    return () => {
      controller.abort();
    };
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      {universes.map(universe => (
        <div key={universe.id}>{universe.name}</div>
      ))}
    </div>
  );
};

export default UniverseList;
Enter fullscreen mode Exit fullscreen mode

good

import { Suspense } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';

const FIVE_MINUTES = 5 * 60 * 1_000;
const TEN_MINUTES  = 10 * 60 * 1_000;

const fetchUniverses = async () => {
  const response = await fetch('/api/universes');

  if (!response.ok) {
    throw new Error(`Error: ${response.status} - ${await response.text()}`);
  }

  const jsonResponse = await response.json();
  return jsonResponse.data || [];
};

const UniverseListContent = () => {
  const { data: universes } = useSuspenseQuery({
    queryKey: ['universes'],
    queryFn: fetchUniverses,
    staleTime: FIVE_MINUTES,
    gcTime: TEN_MINUTES,
    retry: 3,
    retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  return (
    <div>
      {universes.map(universe => (
        <div key={universe.id}>{universe.name}</div>
      ))}
    </div>
  );
};

const ErrorFallback = ({ error, resetErrorBoundary }) => (
  <div role="alert">
    <p>Something went wrong:</p>
    <pre>{error.message}</pre>
    <button onClick={resetErrorBoundary}>Try again</button>
  </div>
);

const UniverseListWithSuspense = () => {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => window.location.reload()}
    >
      <Suspense fallback={<div>Loading universes...</div>}>
        <UniverseListContent />
      </Suspense>
    </ErrorBoundary>
  );
};

export default UniverseListWithSuspense;
Enter fullscreen mode Exit fullscreen mode

Why bad?

Repeated code, required to manage async state, will spread as garden weeds as the project scales.

If you're not willing using React Query, at least create your own decoupled hooks and make sure to properly test them.

caching

React Query can cache your endpoints results and expire then after a configurable expiration time.

It also allows you to tie tags with queries and invalidate them on mutations.

refetching

As easy as calling a function, as it should be.

const UniverseListContent = () => {
  const { data: universes, refetch } = useSuspenseQuery({
    queryKey: ['universes'],
    queryFn: fetchUniverses,
    staleTime: FIVE_MINUTES,
    gcTime: TEN_MINUTES,
    retry: 
    retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  return (
    <div>
      <button onClick={refetch}>Reload All</button>
      {universes.map(universe => (
        <div key={universe.id}>{universe.name}</div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)