Hooks for managing, caching and syncing asynchronous and remote data in React
—
Error boundary integration and reset functionality for graceful error recovery. React Query provides utilities to integrate with React error boundaries and manage error states across queries and mutations.
Error boundary context provider that manages error reset functionality across multiple queries.
/**
* Provides error reset functionality to child components
* @param props - Configuration with children and optional render function
* @returns JSX element with error reset context
*/
function QueryErrorResetBoundary(
props: QueryErrorResetBoundaryProps
): JSX.Element;
interface QueryErrorResetBoundaryProps {
/** Child components or render function */
children:
| ((value: QueryErrorResetBoundaryValue) => React.ReactNode)
| React.ReactNode;
}
interface QueryErrorResetBoundaryValue {
/** Clear the reset flag */
clearReset: () => void;
/** Check if boundary is in reset state */
isReset: () => boolean;
/** Trigger a reset */
reset: () => void;
}Usage Examples:
import { QueryErrorResetBoundary } from "react-query";
// Basic error boundary integration
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<h2>Something went wrong:</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<MainContent />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
// With react-error-boundary library
function AppWithErrorBoundary() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
FallbackComponent={ErrorFallback}
>
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}
function ErrorFallback({ error, resetErrorBoundary }: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert" className="error-fallback">
<h2>Oops! Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>
Try again
</button>
</div>
);
}Hook to access error boundary reset functionality.
/**
* Access error reset boundary controls
* @returns Object with reset control functions
*/
function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue;Usage Examples:
import { useQueryErrorResetBoundary } from "react-query";
// Manual error reset
function ErrorRecoveryComponent() {
const { reset, isReset, clearReset } = useQueryErrorResetBoundary();
const handleRecovery = () => {
// Clear any cached errors
reset();
// Perform additional recovery logic
queryClient.refetchQueries();
// Clear the reset flag
clearReset();
};
return (
<div>
<button onClick={handleRecovery}>
Recover from Errors
</button>
{isReset() && (
<div className="recovery-notice">
Recovery in progress...
</div>
)}
</div>
);
}
// Conditional rendering based on reset state
function ConditionalErrorHandling() {
const { isReset } = useQueryErrorResetBoundary();
if (isReset()) {
return <div>Recovering from errors...</div>;
}
return <MainContent />;
}Creating a comprehensive error boundary with React Query integration:
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
class QueryErrorBoundary extends React.Component<
{ children: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void },
ErrorBoundaryState
> {
constructor(props: any) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return {
hasError: true,
error
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.setState({
error,
errorInfo
});
// Call custom error handler
this.props.onError?.(error, errorInfo);
// Log to error reporting service
console.error('Query Error Boundary caught error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<div className="error-boundary">
<h2>Something went wrong</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
<summary>Error Details</summary>
{this.state.error?.toString()}
<br />
{this.state.errorInfo?.componentStack}
</details>
<div className="error-actions">
<button
onClick={() => {
reset();
this.setState({
hasError: false,
error: null,
errorInfo: null
});
}}
>
Try Again
</button>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
</div>
)}
</QueryErrorResetBoundary>
);
}
return this.props.children;
}
}Handling errors at the individual query level:
function QueryWithErrorHandling() {
const {
data,
error,
isError,
refetch,
isRefetching
} = useQuery({
queryKey: ['sensitive-data'],
queryFn: fetchSensitiveData,
retry: (failureCount, error) => {
// Don't retry authentication errors
if (error.status === 401) {
return false;
}
// Retry server errors up to 3 times
return failureCount < 3;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: (error) => {
// Log error for analytics
analytics.track('query_error', {
queryKey: 'sensitive-data',
error: error.message,
status: error.status
});
// Handle specific error types
if (error.status === 401) {
// Redirect to login
router.push('/login');
} else if (error.status >= 500) {
// Show server error toast
toast.error('Server error occurred. Please try again.');
}
},
useErrorBoundary: (error) => {
// Only throw to error boundary for unexpected errors
return error.status >= 500;
}
});
// Handle error states in component
if (isError && error.status < 500) {
return (
<div className="query-error">
<h3>Unable to load data</h3>
<p>{error.message}</p>
<button
onClick={() => refetch()}
disabled={isRefetching}
>
{isRefetching ? 'Retrying...' : 'Try Again'}
</button>
</div>
);
}
return (
<div>
{data && <DataDisplay data={data} />}
</div>
);
}Setting up global error handling across all queries and mutations:
// Global error handler
const handleGlobalError = (error: any, type: 'query' | 'mutation') => {
// Log to error reporting service
errorReportingService.captureException(error, {
tags: {
type,
component: 'react-query'
}
});
// Handle authentication errors globally
if (error.status === 401) {
// Clear user data and redirect to login
queryClient.setQueryData(['user'], null);
router.push('/login');
return;
}
// Handle rate limiting
if (error.status === 429) {
toast.warning('Too many requests. Please wait a moment.');
return;
}
// Handle server errors
if (error.status >= 500) {
toast.error('Server error occurred. Our team has been notified.');
return;
}
// Handle network errors
if (!navigator.onLine) {
toast.warning('You appear to be offline. Please check your connection.');
return;
}
};
// QueryClient with global error handling
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => handleGlobalError(error, 'query'),
retry: (failureCount, error) => {
// Global retry logic
if (error.status === 401 || error.status === 403) {
return false;
}
if (error.status >= 400 && error.status < 500) {
return false;
}
return failureCount < 3;
}
},
mutations: {
onError: (error) => handleGlobalError(error, 'mutation'),
retry: (failureCount, error) => {
// Be more conservative with mutation retries
if (error.status >= 400 && error.status < 500) {
return false;
}
return failureCount < 1;
}
}
}
});Implementing different error recovery strategies:
function useErrorRecovery() {
const queryClient = useQueryClient();
const { reset } = useQueryErrorResetBoundary();
const recoverFromError = useCallback(async (strategy: 'soft' | 'hard' | 'selective') => {
switch (strategy) {
case 'soft': {
// Just reset error boundary
reset();
break;
}
case 'hard': {
// Clear all cache and reset
queryClient.clear();
reset();
break;
}
case 'selective': {
// Remove only failed queries
queryClient.getQueryCache().getAll().forEach(query => {
if (query.state.status === 'error') {
queryClient.removeQueries({ queryKey: query.queryKey });
}
});
reset();
break;
}
}
}, [queryClient, reset]);
return { recoverFromError };
}
function ErrorRecoveryPanel() {
const { recoverFromError } = useErrorRecovery();
const [isRecovering, setIsRecovering] = useState(false);
const handleRecovery = async (strategy: 'soft' | 'hard' | 'selective') => {
setIsRecovering(true);
try {
await recoverFromError(strategy);
toast.success('Recovery completed successfully!');
} catch (error) {
toast.error('Recovery failed. Please try again.');
} finally {
setIsRecovering(false);
}
};
return (
<div className="error-recovery-panel">
<h3>Error Recovery Options</h3>
<div className="recovery-buttons">
<button
onClick={() => handleRecovery('soft')}
disabled={isRecovering}
>
Soft Reset
</button>
<button
onClick={() => handleRecovery('selective')}
disabled={isRecovering}
>
Clear Failed Queries
</button>
<button
onClick={() => handleRecovery('hard')}
disabled={isRecovering}
>
Full Reset
</button>
</div>
{isRecovering && <div>Recovering...</div>}
</div>
);
}Comprehensive error monitoring setup:
interface ErrorMetrics {
totalErrors: number;
queryErrors: number;
mutationErrors: number;
errorsByType: Record<string, number>;
recentErrors: Array<{
timestamp: number;
type: 'query' | 'mutation';
error: string;
queryKey?: string;
}>;
}
function useErrorMonitoring(): ErrorMetrics {
const [metrics, setMetrics] = useState<ErrorMetrics>({
totalErrors: 0,
queryErrors: 0,
mutationErrors: 0,
errorsByType: {},
recentErrors: []
});
const trackError = useCallback((
error: Error,
type: 'query' | 'mutation',
queryKey?: string
) => {
const errorType = error.name || 'Unknown';
setMetrics(prev => ({
totalErrors: prev.totalErrors + 1,
queryErrors: prev.queryErrors + (type === 'query' ? 1 : 0),
mutationErrors: prev.mutationErrors + (type === 'mutation' ? 1 : 0),
errorsByType: {
...prev.errorsByType,
[errorType]: (prev.errorsByType[errorType] || 0) + 1
},
recentErrors: [
{
timestamp: Date.now(),
type,
error: error.message,
queryKey
},
...prev.recentErrors.slice(0, 9) // Keep last 10 errors
]
}));
// Send to analytics
analytics.track('react_query_error', {
type,
errorType,
message: error.message,
queryKey,
timestamp: Date.now()
});
}, []);
// Set up global error tracking
useEffect(() => {
const queryClient = useQueryClient();
// Override global error handlers to include tracking
const originalQueryOnError = queryClient.getDefaultOptions().queries?.onError;
const originalMutationOnError = queryClient.getDefaultOptions().mutations?.onError;
queryClient.setDefaultOptions({
queries: {
...queryClient.getDefaultOptions().queries,
onError: (error, query) => {
trackError(error as Error, 'query', JSON.stringify(query.queryKey));
originalQueryOnError?.(error, query);
}
},
mutations: {
...queryClient.getDefaultOptions().mutations,
onError: (error, variables, context, mutation) => {
trackError(error as Error, 'mutation', JSON.stringify(mutation.options.mutationKey));
originalMutationOnError?.(error, variables, context, mutation);
}
}
});
}, [trackError]);
return metrics;
}
function ErrorMetricsPanel() {
const metrics = useErrorMonitoring();
return (
<div className="error-metrics">
<h3>Error Metrics</h3>
<div className="metrics-grid">
<div className="metric">
<span className="label">Total Errors:</span>
<span className="value">{metrics.totalErrors}</span>
</div>
<div className="metric">
<span className="label">Query Errors:</span>
<span className="value">{metrics.queryErrors}</span>
</div>
<div className="metric">
<span className="label">Mutation Errors:</span>
<span className="value">{metrics.mutationErrors}</span>
</div>
</div>
<h4>Error Types</h4>
<ul>
{Object.entries(metrics.errorsByType).map(([type, count]) => (
<li key={type}>
{type}: {count}
</li>
))}
</ul>
<h4>Recent Errors</h4>
<ul>
{metrics.recentErrors.map((error, index) => (
<li key={index} className="recent-error">
<span className="timestamp">
{new Date(error.timestamp).toLocaleTimeString()}
</span>
<span className="type">[{error.type}]</span>
<span className="message">{error.error}</span>
</li>
))}
</ul>
</div>
);
}Install with Tessl CLI
npx tessl i tessl/npm-react-query