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