Skip to main content

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:
// 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:
// 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>
  );
}

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();
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 TypeConventionExample
Pagespage.tsxstatistics/page.tsx
Layoutslayout.tsxstatistics/layout.tsx
ComponentsPascalCase.tsxTopNames.tsx
Hooksuse*.ts(x)useStatisticsQuery.ts
Servicesfetch*.tsfetchStatistics.ts
UtilscamelCase.tsgroupBy.ts
Types*.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 />
    </>
  );
}

Performance Optimizations

  • 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:
// Every page exports generateMetadata
export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    title: 'Page Title',
    description: 'Page description for search engines',
    alternates: {
      canonical: 'https://tamborradata.com/page',
    },
    openGraph: {
      title: 'OG Title',
      description: 'OG Description',
      url: 'https://tamborradata.com/page',
      type: 'article',
      images: [{ url: '/og-image.webp' }],
    },
    twitter: {
      card: 'summary_large_image',
      title: 'Twitter Title',
      description: 'Twitter Description',
    },
  };
}

Next Steps

Backend

Learn about API Routes, Services, and Repositories

Database

Explore PostgreSQL schema and RLS policies

Build docs developers (and LLMs) love