Skip to main content

Overview

TamborraData leverages React Query 5 (@tanstack/query) as the core server state management solution, providing automatic caching, background synchronization, and seamless SSR integration with Next.js 16.
React Query eliminates the need for manual state management, loading states, and cache invalidation logic, reducing boilerplate by ~70% compared to traditional Redux patterns.

Why React Query?

Key Advantages

React Query automatically caches server responses with configurable TTL, eliminating redundant network requests and improving perceived performance.
// Multiple components call this hook
// Only 1 HTTP request is made
useStatisticsQuery('2024');
Data stays fresh through smart background refetching based on window focus, network reconnection, and time intervals.
First-class integration with Next.js Server Components enables instant content delivery and optimal SEO.
Identical requests made simultaneously are automatically deduplicated into a single network call.

Compared Alternatives

FeatureReact QuerySWRRedux RTK Query
Infinite cache⚠️ Manual
Conditional polling
Next.js 16 SSR⚠️ Limited
Type safety
Bundle size13kb4kb45kb
Request cancellation
DevTools
React Query provides the best balance between features and simplicity for data-heavy applications.

Global Configuration

The query client is configured in ReactQueryProvider.tsx with optimized defaults:
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,        // 5 minutes
      gcTime: 30 * 60 * 1000,          // 30 minutes
      retry: 1,                         // 1 retry on failure
      refetchOnWindowFocus: false,      // No refetch on tab focus
      refetchOnReconnect: false,        // No refetch on reconnect
    },
    mutations: {
      retry: 0,
    },
  },
});

Configuration Explained

5 minutes - Historical statistics data doesn’t change frequently, so we can safely cache for extended periods.
staleTime: 5 * 60 * 1000  // Data stays fresh for 5 minutes

Caching Strategies

TamborraData implements three distinct caching patterns based on data characteristics:

1. Infinite Cache (Static Data)

For immutable historical data that never changes:
export function useStatisticsQuery(year: string) {
  return useQuery({
    queryKey: queryKeys.statistics(year),
    queryFn: () => fetchStatistics(year),
    staleTime: Infinity,  // Never marked as stale
    gcTime: Infinity,     // Never garbage collected
  });
}
Used for annual statistics, expanded categories, and historical aggregates.

2. Conditional Polling (Dynamic Data)

For data that changes during update periods:
export function useStatisticsQuery(year: string) {
  return useQuery({
    queryKey: queryKeys.statistics(year),
    queryFn: () => fetchStatistics(year),
    
    // Poll every 3 seconds when system is updating
    refetchInterval: (query) =>
      query.state.data?.isUpdating ? 3000 : false,
    
    // Re-fetch on focus if updating
    refetchOnWindowFocus: (query) =>
      query.state.data?.isUpdating === true,
  });
}
Behavior:
  • isUpdating = true → Poll every 3 seconds
  • isUpdating = false → No polling
  • User returns to tab → Resume polling if updating

3. Short TTL (Search Results)

For user searches with moderate volatility:
export function useParticipantsQuery(params: SearchParams) {
  return useQuery({
    queryKey: queryKeys.participants(params),
    queryFn: () => fetchParticipants(params),
    staleTime: 2 * 60 * 1000,   // 2 minutes
    gcTime: 10 * 60 * 1000,     // 10 minutes
  });
}

Custom Hooks

All React Query usage is encapsulated in custom hooks for better organization and reusability.

useStatisticsQuery

export function useStatisticsQuery<T extends StatsResponse>(year: string) {
  return useQuery({
    queryKey: queryKeys.statistics(year),
    queryFn: ({ signal }) => fetchStatistics<T>(year, signal),
    enabled: Boolean(year),
    staleTime: Infinity,
    gcTime: Infinity,
    retry: 0,
    refetchOnWindowFocus: (query) => query.state.data?.isUpdating === true,
    refetchInterval: (query) => (query.state.data?.isUpdating ? 3000 : false),
  });
}
Features:
  • ✅ AbortController support for request cancellation
  • ✅ Conditional polling based on isUpdating flag
  • ✅ Generic type support for different response shapes
  • ✅ Infinite cache for historical data

useCategoryQuery

export function useCategoryQuery(year: string, category: string) {
  return useQuery({
    queryKey: queryKeys.category(year, category),
    queryFn: () => fetchCategory(year, category),
    enabled: Boolean(year && category),
    staleTime: Infinity,
    gcTime: Infinity,
  });
}

useYearsQuery

export function useYearsQuery() {
  return useQuery({
    queryKey: queryKeys.years(),
    queryFn: fetchYears,
    staleTime: Infinity,
    gcTime: Infinity,
  });
}

SSR and Prefetching

TamborraData uses Next.js 16 Server Components with React Query for optimal SSR performance.

Prefetching Strategy

// page.tsx (Server Component)
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient } from '@/lib/getQueryClient';

export default async function StatisticsPage({ params }: Props) {
  const queryClient = getQueryClient();

  // Prefetch on server
  await queryClient.prefetchQuery({
    queryKey: queryKeys.statistics(params.year),
    queryFn: () => fetchStatistics(params.year),
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <StatisticsContent year={params.year} />
    </HydrationBoundary>
  );
}

SSR Data Flow

1

Server Prefetch

Server component prefetches data and populates React Query cache.
await queryClient.prefetchQuery({
  queryKey: ['statistics', '2024'],
  queryFn: () => fetchStatistics('2024'),
});
2

Dehydration

Cache is serialized and embedded in the HTML response.
const dehydratedState = dehydrate(queryClient);
3

HTML Delivery

Browser receives fully rendered HTML with embedded data.
4

Client Hydration

React Query rehydrates cache on client, avoiding duplicate requests.
<HydrationBoundary state={dehydratedState}>
  <Component />
</HydrationBoundary>
5

Instant Display

Component reads from cache immediately - no loading state!
const { data } = useStatisticsQuery('2024');
// data is immediately available
With SSR prefetching, users see content instantly and search engines get fully rendered HTML for optimal SEO.

Conditional Polling System

The polling system is TamborraData’s most sophisticated React Query feature.

Why Polling?

During January, the system generates new data automatically. The frontend detects this via the isUpdating flag and activates real-time polling.

Implementation

refetchInterval: (query) => {
  const isUpdating = query.state.data?.isUpdating;
  return isUpdating ? 3000 : false;
};

refetchOnWindowFocus: (query) => {
  // Only refetch if currently updating
  return query.state.data?.isUpdating === true;
};

Behavior Matrix

System StatePolling ActiveRefetch on Focus
isUpdating = true✅ Every 3s✅ Yes
isUpdating = false❌ No❌ No
Network error❌ Stops❌ No
Polling consumes server resources. Always implement conditional logic to prevent unnecessary requests.

Query Keys Organization

Structured query keys enable precise cache invalidation:
// lib/queryKeys.ts
export const queryKeys = {
  all: ['tamborradata'] as const,
  statistics: (year: string) => 
    [...queryKeys.all, 'statistics', year] as const,
  category: (year: string, cat: string) => 
    [...queryKeys.statistics(year), cat] as const,
  years: () => 
    [...queryKeys.all, 'years'] as const,
  companies: () => 
    [...queryKeys.all, 'companies'] as const,
  participants: (params: SearchParams) => 
    [...queryKeys.all, 'participants', params] as const,
};

Benefits

Selective Invalidation

Invalidate specific queries without affecting others:
// Invalidate all 2024 statistics
queryClient.invalidateQueries({
  queryKey: queryKeys.statistics('2024')
});

Type Safety

TypeScript ensures query keys are used correctly:
const key = queryKeys.statistics('2024');
// ['tamborradata', 'statistics', '2024']

Easy Debugging

Hierarchical structure makes cache inspection simple in React Query DevTools.

Consistency

Centralized keys prevent typos and ensure consistency across the codebase.

Performance Optimizations

Request Deduplication

React Query automatically deduplicates identical requests:
// Multiple components call this hook simultaneously
<ComponentA />  ──┐
<ComponentB />  ──┤→ 1 single HTTP request
<ComponentC />  ──┘

useStatisticsQuery('2024');

Request Cancellation

queryFn: ({ signal }) => fetchStatistics(year, signal);

// In fetchStatistics
export async function fetchStatistics(year: string, signal?: AbortSignal) {
  const response = await fetch(`/api/statistics?year=${year}`, { signal });
  return response.json();
}
Benefit: If users navigate away quickly, in-flight requests are cancelled automatically.

Lazy Hydration

<HydrationBoundary state={dehydrate(queryClient)}>
  {/* Only rehydrates when component mounts */}
  <StatisticsContent />
</HydrationBoundary>

Best Practices

Always wrap useQuery in custom hooks for better organization:
// ✅ Good
export function useStatisticsQuery(year: string) {
  return useQuery({
    queryKey: queryKeys.statistics(year),
    queryFn: () => fetchStatistics(year),
  });
}

// ❌ Bad
useQuery({
  queryKey: ['statistics', year],
  queryFn: () => fetch('...')
});
For immutable data, use infinite cache to eliminate unnecessary refetches:
staleTime: Infinity,
gcTime: Infinity,
Use dynamic options for smart caching:
refetchInterval: (query) => 
  query.state.data?.needsPolling ? 3000 : false,
Always pass AbortSignal to fetch calls:
queryFn: ({ signal }) => fetch(url, { signal }),

Resources

React Query Docs

Official TanStack Query documentation

SSR Guide

Server-side rendering with React Query

Advanced Patterns

Practical React Query patterns by TkDodo

Next.js Integration

Advanced SSR patterns with Next.js

Build docs developers (and LLMs) love