Hooks for managing, caching and syncing asynchronous and remote data in React
—
Execute multiple queries in parallel with type-safe results and coordinated loading states. The useQueries hook allows you to run multiple queries simultaneously while maintaining individual query states.
The main hook for executing multiple queries in parallel with full type safety.
/**
* Execute multiple queries in parallel with type-safe results
* @param config - Configuration object with queries array and optional context
* @returns Array of query results matching input query types
*/
function useQueries<T extends any[]>({
queries,
context,
}: {
queries: readonly [...QueriesOptions<T>];
context?: React.Context<QueryClient | undefined>;
}): QueriesResults<T>;Usage Examples:
import { useQueries } from "react-query";
// Basic parallel queries
const results = useQueries({
queries: [
{
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId
},
{
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
enabled: !!userId
},
{
queryKey: ['settings'],
queryFn: fetchSettings
}
]
});
const [userQuery, postsQuery, settingsQuery] = results;
// Type-safe access to individual results
const user = userQuery.data;
const posts = postsQuery.data;
const settings = settingsQuery.data;
// Check loading states
const isLoadingAny = results.some(result => result.isLoading);
const isLoadingAll = results.every(result => result.isLoading);
// Dynamic parallel queries
function UserDashboard({ userIds }: { userIds: string[] }) {
const userQueries = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000 // 5 minutes
}))
});
const users = userQueries.map(query => query.data).filter(Boolean);
const isLoading = userQueries.some(query => query.isLoading);
const hasError = userQueries.some(query => query.isError);
if (isLoading) return <div>Loading users...</div>;
if (hasError) return <div>Error loading some users</div>;
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}Advanced type inference for query options and results.
/**
* Complex type system for inferring query options and results
* Supports explicit type parameters and automatic inference
*/
type QueriesOptions<
T extends any[],
Result extends any[] = [],
Depth extends ReadonlyArray<number> = []
> = Depth['length'] extends 20 // Maximum depth limit
? UseQueryOptions[]
: T extends []
? []
: T extends [infer Head]
? [...Result, GetOptions<Head>]
: T extends [infer Head, ...infer Tail]
? QueriesOptions<[...Tail], [...Result, GetOptions<Head>], [...Depth, 1]>
: T extends UseQueryOptions<infer TQueryFnData, infer TError, infer TData, infer TQueryKey>[]
? UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>[]
: UseQueryOptions[];
type QueriesResults<
T extends any[],
Result extends any[] = [],
Depth extends ReadonlyArray<number> = []
> = Depth['length'] extends 20 // Maximum depth limit
? UseQueryResult[]
: T extends []
? []
: T extends [infer Head]
? [...Result, GetResults<Head>]
: T extends [infer Head, ...infer Tail]
? QueriesResults<[...Tail], [...Result, GetResults<Head>], [...Depth, 1]>
: T extends UseQueryOptions<infer TQueryFnData, infer TError, infer TData, any>[]
? UseQueryResult<unknown extends TData ? TQueryFnData : TData, TError>[]
: UseQueryResult[];Each query in the array uses standard UseQueryOptions without the context property.
type UseQueryOptionsForUseQueries<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'context'>;Running queries conditionally while maintaining type safety:
function ConditionalDashboard({ userId, isAdmin }: { userId: string; isAdmin: boolean }) {
const queries = useQueries({
queries: [
// Always fetch user data
{
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
},
// Only fetch admin data if user is admin
{
queryKey: ['admin-stats'],
queryFn: fetchAdminStats,
enabled: isAdmin
},
// Conditional query based on user data
{
queryKey: ['user-preferences', userId],
queryFn: () => fetchUserPreferences(userId),
enabled: !!userId
}
]
});
const [userQuery, adminStatsQuery, preferencesQuery] = queries;
// Handle different loading states
const criticalDataLoading = userQuery.isLoading;
const optionalDataLoading = adminStatsQuery.isLoading || preferencesQuery.isLoading;
return (
<div>
{criticalDataLoading ? (
<div>Loading user data...</div>
) : (
<div>
<h1>{userQuery.data?.name}</h1>
{isAdmin && adminStatsQuery.data && (
<AdminPanel stats={adminStatsQuery.data} />
)}
{preferencesQuery.data && (
<PreferencesPanel preferences={preferencesQuery.data} />
)}
</div>
)}
</div>
);
}Generating queries based on runtime data:
interface DashboardData {
userId: string;
widgets: Array<{
id: string;
type: 'analytics' | 'notifications' | 'activity';
config: Record<string, any>;
}>;
}
function DynamicDashboard({ dashboardData }: { dashboardData: DashboardData }) {
const queries = useQueries({
queries: [
// Base user query
{
queryKey: ['user', dashboardData.userId],
queryFn: () => fetchUser(dashboardData.userId)
},
// Dynamic widget queries
...dashboardData.widgets.map(widget => ({
queryKey: ['widget', widget.id, widget.type],
queryFn: () => fetchWidgetData(widget.type, widget.config),
staleTime: widget.type === 'analytics' ? 5 * 60 * 1000 : 30 * 1000
}))
]
});
const [userQuery, ...widgetQueries] = queries;
const isLoadingCritical = userQuery.isLoading;
const isLoadingWidgets = widgetQueries.some(q => q.isLoading);
return (
<div>
{isLoadingCritical ? (
<div>Loading dashboard...</div>
) : (
<div>
<UserHeader user={userQuery.data} />
<div className="widgets-grid">
{dashboardData.widgets.map((widget, index) => {
const widgetQuery = widgetQueries[index];
return (
<Widget
key={widget.id}
type={widget.type}
data={widgetQuery.data}
isLoading={widgetQuery.isLoading}
error={widgetQuery.error}
/>
);
})}
</div>
</div>
)}
</div>
);
}Handling errors across multiple parallel queries:
function RobustParallelQueries() {
const queries = useQueries({
queries: [
{
queryKey: ['critical-data'],
queryFn: fetchCriticalData,
retry: 3
},
{
queryKey: ['optional-data'],
queryFn: fetchOptionalData,
retry: 1,
onError: () => {
// Log optional data errors but don't show to user
console.warn('Optional data failed to load');
}
},
{
queryKey: ['fallback-data'],
queryFn: fetchFallbackData,
retry: false
}
]
});
const [criticalQuery, optionalQuery, fallbackQuery] = queries;
// Critical error handling
if (criticalQuery.isError) {
return (
<ErrorState
error={criticalQuery.error}
onRetry={() => criticalQuery.refetch()}
/>
);
}
// Show loading state while critical data loads
if (criticalQuery.isLoading) {
return <LoadingState />;
}
return (
<div>
<CriticalDataComponent data={criticalQuery.data} />
{optionalQuery.data && (
<OptionalDataComponent data={optionalQuery.data} />
)}
{optionalQuery.isError && (
<div className="warning">
Some features may be limited due to data loading issues.
</div>
)}
{fallbackQuery.data && (
<FallbackDataComponent data={fallbackQuery.data} />
)}
</div>
);
}Tracking performance metrics across parallel queries:
function MonitoredParallelQueries() {
const startTime = useRef(Date.now());
const [metrics, setMetrics] = useState({
totalQueries: 0,
completedQueries: 0,
failedQueries: 0,
averageLoadTime: 0
});
const queries = useQueries({
queries: [
{
queryKey: ['user-data'],
queryFn: () => fetchUserData(),
onSuccess: () => trackQuerySuccess('user-data'),
onError: () => trackQueryError('user-data')
},
{
queryKey: ['analytics'],
queryFn: () => fetchAnalytics(),
onSuccess: () => trackQuerySuccess('analytics'),
onError: () => trackQueryError('analytics')
},
{
queryKey: ['notifications'],
queryFn: () => fetchNotifications(),
onSuccess: () => trackQuerySuccess('notifications'),
onError: () => trackQueryError('notifications')
}
]
});
const trackQuerySuccess = (queryName: string) => {
const loadTime = Date.now() - startTime.current;
console.log(`${queryName} loaded in ${loadTime}ms`);
setMetrics(prev => ({
...prev,
completedQueries: prev.completedQueries + 1,
averageLoadTime: (prev.averageLoadTime + loadTime) / 2
}));
};
const trackQueryError = (queryName: string) => {
console.error(`${queryName} failed to load`);
setMetrics(prev => ({
...prev,
failedQueries: prev.failedQueries + 1
}));
};
const allSettled = queries.every(q => !q.isLoading);
const hasErrors = queries.some(q => q.isError);
useEffect(() => {
if (allSettled) {
const totalTime = Date.now() - startTime.current;
console.log(`All queries settled in ${totalTime}ms`);
// Send analytics
analytics.track('parallel_queries_completed', {
totalTime,
queryCount: queries.length,
errorCount: metrics.failedQueries,
successCount: metrics.completedQueries
});
}
}, [allSettled]);
return (
<div>
{/* Development metrics display */}
{process.env.NODE_ENV === 'development' && (
<div className="debug-metrics">
<p>Queries: {queries.length}</p>
<p>Completed: {metrics.completedQueries}</p>
<p>Failed: {metrics.failedQueries}</p>
<p>Avg Load Time: {metrics.averageLoadTime.toFixed(0)}ms</p>
</div>
)}
{/* Main content */}
{queries.map((query, index) => (
<QueryResult key={index} query={query} />
))}
</div>
);
}Creating reusable query configurations:
// Query factory functions
const createUserQuery = (userId: string) => ({
queryKey: ['user', userId] as const,
queryFn: () => fetchUser(userId),
enabled: !!userId
});
const createPostsQuery = (userId: string) => ({
queryKey: ['posts', userId] as const,
queryFn: () => fetchUserPosts(userId),
enabled: !!userId
});
const createAnalyticsQuery = (dateRange: DateRange) => ({
queryKey: ['analytics', dateRange] as const,
queryFn: () => fetchAnalytics(dateRange),
staleTime: 5 * 60 * 1000
});
// Using query factories
function TypeSafeDashboard({ userId, dateRange }: { userId: string; dateRange: DateRange }) {
const queries = useQueries({
queries: [
createUserQuery(userId),
createPostsQuery(userId),
createAnalyticsQuery(dateRange)
]
});
const [userQuery, postsQuery, analyticsQuery] = queries;
// TypeScript knows the exact types of each query result
return (
<div>
{userQuery.data && <h1>{userQuery.data.name}</h1>}
{postsQuery.data && <PostsList posts={postsQuery.data} />}
{analyticsQuery.data && <AnalyticsChart data={analyticsQuery.data} />}
</div>
);
}Install with Tessl CLI
npx tessl i tessl/npm-react-query