Frontend Architecture
TamborraData’s frontend is built with Next.js 16 App Router and uses React Server Components as the default rendering strategy, with selective use of Client Components for interactivity.
Technology Stack
Next.js 16 App Router, Server Components, and API Routes
React 19 Server Components, Client Components, and hooks
TanStack Query Server state management and caching
TailwindCSS Utility-first CSS framework
Server Components by Default
TamborraData follows the Server Components First principle:
Server Component (Default)
Client Component (Selective)
// app/(frontend)/statistics/[year]/page.tsx
import type { Metadata } from 'next' ;
import { YearPageContent } from './YearPageContent' ;
// ✅ No 'use client' = Server Component
export async function generateMetadata ({
params ,
} : {
params : Promise <{ year : string }>;
}) : Promise < Metadata > {
const { year } = await params ;
return {
title: `Estadísticas de la Tamborrada Infantil ${ year } ` ,
description: `Análisis de la Tamborrada Infantil ${ year } ` ,
alternates: {
canonical: `https://tamborradata.com/statistics/ ${ year } ` ,
},
};
}
export default async function YearPage ({
params
} : {
params : Promise <{ year : string }>
}) {
const { year } = await params ;
return (
<>
< YearStructuredData year = { year } />
< YearPageContent />
</>
);
}
When to Use Client Components
Only use 'use client' when you need:
useState, useEffect, useReducer
Custom hooks that use state
React Query hooks (useQuery, useMutation)
'use client' ;
import { useState } from 'react' ;
export function Counter () {
const [ count , setCount ] = useState ( 0 );
return < button onClick = { () => setCount ( count + 1 ) } > { count } </ button > ;
}
onClick, onChange, onSubmit
User interactions
Form handling
'use client' ;
export function SearchForm () {
const handleSubmit = ( e : FormEvent ) => {
e . preventDefault ();
// Handle form submission
};
return < form onSubmit = { handleSubmit } > ... </ form > ;
}
window, document, localStorage
Geolocation, Web APIs
Third-party browser libraries
'use client' ;
import { useEffect , useState } from 'react' ;
export function WindowSize () {
const [ width , setWidth ] = useState ( 0 );
useEffect (() => {
setWidth ( window . innerWidth );
}, []);
return < div > Width: { width } px </ div > ;
}
Framer Motion
React Spring
GSAP with React
'use client' ;
import { motion } from 'framer-motion' ;
export function AnimatedCard () {
return (
< motion.div
initial = { { opacity: 0 } }
animate = { { opacity: 1 } }
>
Content
</ motion.div >
);
}
Avoid unnecessary Client Components! Each 'use client' directive adds JavaScript to the client bundle and disables server-side optimizations.
React Query for State Management
TamborraData uses TanStack React Query as the single source of truth for server state:
Provider Setup
Query Hook
HTTP Service
Usage in Component
// app/(frontend)/providers/ReactQueryProvider.tsx
'use client' ;
import { QueryClient , QueryClientProvider } from '@tanstack/react-query' ;
import { ReactQueryDevtools } from '@tanstack/react-query-devtools' ;
export const queryClient = new QueryClient ({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000 , // 5 minutes
gcTime: 30 * 60 * 1000 , // 30 minutes
retry: 1 ,
refetchOnWindowFocus: false ,
refetchOnReconnect: false ,
},
},
});
export function ReactQueryProvider ({ children } : { children : React . ReactNode }) {
return (
< QueryClientProvider client = { queryClient } >
{ children }
< ReactQueryDevtools initialIsOpen = { false } />
</ QueryClientProvider >
);
}
// hooks/query/useStatisticsQuery.ts
import { useQuery } from '@tanstack/react-query' ;
import { fetchStatistics } from '@/services/fetchStatistics' ;
import { queryKeys } from '@/lib/queryKeys' ;
export function useStatisticsQuery < T >( year : string ) {
return useQuery ({
queryKey: queryKeys . statistics ( year ),
queryFn : ({ signal }) => fetchStatistics < T >( year , signal ),
enabled: Boolean ( year ),
staleTime: Infinity ,
gcTime: Infinity ,
retry: 0 ,
// Refetch if system is updating
refetchOnWindowFocus : ( query ) =>
query . state . data ?. isUpdating === true ,
refetchInterval : ( query ) =>
query . state . data ?. isUpdating ? 3000 : false ,
});
}
// services/fetchStatistics.ts
export async function fetchStatistics < T >(
year : string ,
signal : AbortSignal
) : Promise < T > {
const response = await fetch (
`/api/statistics?year= ${ year } ` ,
{ signal }
);
const data = await response . json ();
if ( ! response . ok ) {
const error : any = new Error ( data ?. error || 'Unknown error' );
error . status = response . status ;
throw error ;
}
return data as T ;
}
// components/StatsTable.tsx
'use client' ;
import { useStatisticsQuery } from '@/hooks/query/useStatisticsQuery' ;
export function StatsTable ({ year } : { year : string }) {
const { data , isLoading , error } = useStatisticsQuery ( year );
if ( isLoading ) return < LoadingSkeleton /> ;
if ( error ) return < ErrorMessage error = { error } /> ;
return (
< table >
{ /* Render statistics */ }
</ table >
);
}
State Management Strategy
Principle: Use React Query for server state, use native React hooks for UI state.
Use React Query for:
API data (statistics, participants, years)
Cached responses
Background refetching
Loading/error states
// ✅ Server state with React Query
const { data : statistics } = useStatisticsQuery ( '2024' );
const { data : participants } = useParticipantsQuery ( 'John' );
const { data : years } = useYearsQuery ();
Use useState/useReducer for:
Form inputs
Modal open/closed
Selected filters
Animations
// ✅ UI state with useState
const [ isModalOpen , setIsModalOpen ] = useState ( false );
const [ selectedCategory , setSelectedCategory ] = useState ( 'all' );
const [ searchQuery , setSearchQuery ] = useState ( '' );
Don’t use Redux or Zustand! React Query handles server state better than global state managers. For UI state, native React hooks are sufficient.
Project Structure
Frontend code is organized by feature:
app/(frontend)/
├── statistics/ # Statistics feature
│ ├── page.tsx # Main page (Server Component)
│ ├── [year]/
│ │ ├── page.tsx # Dynamic year page
│ │ ├── layout.tsx # Year-specific layout
│ │ ├── components/ # Year-specific components
│ │ │ ├── TopNames/
│ │ │ │ ├── TopNames.tsx
│ │ │ │ ├── components/
│ │ │ │ └── hooks/
│ │ │ └── TopSchools/
│ │ ├── hooks/ # Year-specific hooks
│ │ │ └── useCategory.tsx
│ │ └── YearPageContent.tsx
│ ├── global/ # Global statistics
│ │ └── page.tsx
│ └── components/ # Shared statistics components
│ ├── StatsWrapper.tsx
│ ├── ErrorPage.tsx
│ └── loaders/
├── search/ # Search feature
│ ├── page.tsx
│ ├── components/
│ │ ├── SearchCard/
│ │ ├── Hero/
│ │ └── FAQs/
│ └── hooks/
│ ├── useParticipants.ts
│ └── useCompanies.ts
├── components/ # Shared components
│ ├── Header/
│ ├── SearchParticipant/
│ └── ExploreStatistics/
├── hooks/ # Shared hooks
│ └── query/ # React Query hooks
│ ├── useStatisticsQuery.ts
│ ├── useParticipantsQuery.ts
│ ├── useYearsQuery.ts
│ └── useCompaniesQuery.ts
├── services/ # HTTP services
│ ├── fetchStatistics.ts
│ ├── fetchParticipants.ts
│ ├── fetchYears.ts
│ └── fetchCompanies.ts
├── lib/ # Utilities
│ └── queryKeys.ts # React Query keys
├── providers/ # Context providers
│ └── ReactQueryProvider.tsx
├── page.tsx # Homepage
└── layout.tsx # Root layout
File Naming Conventions
File Type Convention Example Pages page.tsxstatistics/page.tsxLayouts layout.tsxstatistics/layout.tsxComponents PascalCase.tsxTopNames.tsxHooks use*.ts(x)useStatisticsQuery.tsServices fetch*.tsfetchStatistics.tsUtils camelCase.tsgroupBy.tsTypes *.types.ts or index.tsstatistics.types.ts
Component Patterns
// app/(frontend)/statistics/[year]/page.tsx
import type { Metadata } from 'next' ;
import { YearPageContent } from './YearPageContent' ;
import { YearStructuredData } from './YearStructuredData' ;
// Generate metadata for SEO
export async function generateMetadata ({
params ,
} : {
params : Promise <{ year : string }>;
}) : Promise < Metadata > {
const { year } = await params ;
return {
title: `Statistics ${ year } ` ,
description: `Analysis for ${ year } ` ,
};
}
// Server Component - no 'use client'
export default async function YearPage ({
params
} : {
params : Promise <{ year : string }>
}) {
const { year } = await params ;
return (
<>
< YearStructuredData year = { year } />
< YearPageContent />
</>
);
}
// components/TopNames/TopNames.tsx
'use client' ;
import { useTopNamesQuery } from './hooks/useTopNames' ;
import { TopNamesTable } from './components/TopNamesTable' ;
import { TopNamesChart } from './components/TopNamesChart' ;
export function TopNames ({ year } : { year : string }) {
const { data , isLoading , error } = useTopNamesQuery ( year );
if ( isLoading ) return < LoadingTable /> ;
if ( error ) return < ErrorMessage /> ;
if ( ! data ) return null ;
return (
< div >
< h2 > Top Names { year } </ h2 >
< TopNamesTable data = { data } />
< TopNamesChart data = { data } />
</ div >
);
}
// hooks/useTopNames.ts
import { useMemo } from 'react' ;
import { useStatisticsQuery } from '@/hooks/query/useStatisticsQuery' ;
export function useTopNamesQuery ( year : string ) {
const { data , ... rest } = useStatisticsQuery ( year );
const topNames = useMemo (() => {
if ( ! data ?. statistics ) return null ;
return data . statistics [ 'top-names' ]?. public_data || [];
}, [ data ]);
return {
data: topNames ,
... rest ,
};
}
// components/LoadingTable.tsx
export function LoadingTable () {
return (
< div className = "animate-pulse" >
< div className = "h-8 bg-gray-200 rounded mb-4" />
< div className = "h-64 bg-gray-200 rounded" />
</ div >
);
}
Zero JS by default: Server Components ship no JavaScript to the client
Direct database access: Can query databases directly (though we use API routes)
Streaming: Can stream content as it’s ready
SEO-friendly: Fully rendered HTML sent to browser
Example: app/(frontend)/statistics/[year]/page.tsx:46
Stale time: Data considered fresh for 5 minutes
Garbage collection: Cache cleared after 30 minutes
Deduplication: Multiple components requesting same data only trigger one request
Background refetch: Stale data refetched in background
Configuration: app/(frontend)/providers/ReactQueryProvider.tsx:6
Automatic: Next.js automatically splits code by route
Dynamic imports: Use dynamic() for heavy components
Client boundaries: Each 'use client' is a split point
import dynamic from 'next/dynamic' ;
const HeavyChart = dynamic (() => import ( './HeavyChart' ), {
loading : () => < LoadingChart /> ,
ssr: false , // Don't render on server if it uses browser APIs
});
Next.js Image: Automatic optimization and lazy loading
WebP format: Modern format with better compression
Responsive images: Serve appropriate size for device
import Image from 'next/image' ;
< Image
src = "/hero.webp"
alt = "Tamborradata"
width = { 1200 }
height = { 600 }
priority // Load eagerly for above-the-fold images
/>
SEO Best Practices
TamborraData implements comprehensive SEO:
Next Steps
Backend Learn about API Routes, Services, and Repositories
Database Explore PostgreSQL schema and RLS policies