Caching and React Developer Guide
This document provides a comprehensive overview of how we use React 19, Next.js 15.2, and various caching strategies in our monorepo. It covers rendering patterns, component composition, routing, data fetching, and caching implementations.
Table of Contents
- Rendering Strategies Overview
- React 19 Component Patterns
- Next.js 15.2 App Router
- Data Fetching Strategies
- Caching Implementation
- Performance Best Practices
Rendering Strategies Overview
Our monorepo utilizes multiple rendering strategies based on the specific needs of each application and page type.
CSR (Client-Side Rendering)
When to use:
- Highly interactive applications with frequent state changes
- Real-time dashboards and trading interfaces
- Components that require browser-specific APIs
Implementation:
// Force client-side rendering
'use client';
import { useState, useEffect } from 'react';
/**
* Client-side rendered component for real-time data
* @description Handles dynamic user interactions and state management
* @returns {JSX.Element} Interactive trading component
*/
export default function TradingDashboard(): JSX.Element {
const [positions, setPositions] = useState([]);
useEffect(() => {
// Client-side data fetching for real-time updates
const fetchPositions = async () => {
const response = await fetch('/api/positions');
const data = await response.json();
setPositions(data);
};
fetchPositions();
}, []);
return (
<div>
{/* Interactive trading interface */}
</div>
);
}
SSR (Server-Side Rendering)
When to use:
- SEO-critical pages
- Pages with dynamic content that changes per request
- User-specific content that cannot be cached
Implementation:
// pages/account/[id].tsx
export const dynamic = 'force-dynamic'; // Ensures SSR for every request
/**
* Server-side rendered account page
* @description Generates HTML on server for each request
* @returns {Promise<JSX.Element>} Account details page
*/
export default async function AccountPage({
params
}: {
params: { id: string }
}): Promise<JSX.Element> {
// Data is fetched on the server for each request
const accountData = await getAccountData(params.id);
return (
<div>
<h1>Account {accountData.accountNumber}</h1>
{/* User-specific content */}
</div>
);
}
ISR (Incremental Static Regeneration)
When to use:
- Content that updates periodically
- High-traffic pages with mostly static content
- Marketing pages with occasional updates
Implementation:
// Revalidate every 30 minutes
export const revalidate = 1800;
/**
* ISR page for market data
* @description Pre-rendered at build time, regenerated on demand
* @returns {Promise<JSX.Element>} Market overview page
*/
export default async function MarketOverview(): Promise<JSX.Element> {
const marketData = await getMarketData();
return (
<div>
<h1>Market Overview</h1>
<p>Last updated: {marketData.lastUpdated}</p>
{/* Market data content */}
</div>
);
}
SSG (Static Site Generation)
When to use:
- Documentation pages
- Landing pages
- Content that rarely changes
Implementation:
export const dynamic = 'force-static';
/**
* Static page generation for documentation
* @description Pre-rendered at build time, cached indefinitely
* @returns {Promise<JSX.Element>} Documentation page
*/
export default async function DocumentationPage(): Promise<JSX.Element> {
const docs = await getDocumentationContent();
return (
<div>
<h1>{docs.title}</h1>
<div dangerouslySetInnerHTML={{ __html: docs.content }} />
</div>
);
}
/**
* Generate static paths for all documentation pages
* @description Defines which pages to pre-render at build time
* @returns {Promise<{paths: string[], fallback: boolean}>} Static generation paths
*/
export async function generateStaticParams(): Promise<{ slug: string }[]> {
const docs = await getAllDocumentationSlugs();
return docs.map((doc) => ({
slug: doc.slug,
}));
}
PPR (Partial Pre-Rendering)
When to use:
- Pages with both static and dynamic content
- E-commerce product pages
- Mixed content scenarios
Implementation:
// Enable PPR for mixed content
import { Suspense } from 'react';
/**
* Partial pre-rendering implementation
* @description Combines static shell with dynamic content
* @returns {JSX.Element} Mixed static/dynamic page
*/
export default function ProductPage({ params }: { params: { id: string } }): JSX.Element {
return (
<div>
{/* Static content - pre-rendered */}
<header>
<h1>Product Details</h1>
<nav>Static Navigation</nav>
</header>
{/* Dynamic content - rendered on demand */}
<Suspense fallback={<ProductSkeleton />}>
<DynamicProductInfo productId={params.id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<DynamicReviews productId={params.id} />
</Suspense>
{/* Static footer - pre-rendered */}
<footer>Static Footer Content</footer>
</div>
);
}
React 19 Component Patterns
Component Composition with React.FC
While we primarily use function components, here's how we structure them for maximum type safety and reusability:
import { ReactNode } from 'react';
interface ButtonProps {
children: ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: () => void;
}
/**
* Reusable button component with variant styling
* @description Implements design system button patterns
* @param {ButtonProps} props - Button configuration
* @returns {JSX.Element} Styled button element
*/
export default function Button({
children,
variant = 'primary',
size = 'medium',
disabled = false,
onClick
}: ButtonProps): JSX.Element {
const baseClasses = 'font-medium rounded-md focus:outline-none focus:ring-2';
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
};
const sizeClasses = {
small: 'px-3 py-2 text-sm',
medium: 'px-4 py-2 text-base',
large: 'px-6 py-3 text-lg'
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
Container and Layout Components
interface ContainerProps {
children: ReactNode;
margin?: 'sm' | 'md' | 'lg' | 'xl';
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
/**
* Layout container component
* @description Provides consistent spacing and max-width constraints
* @param {ContainerProps} props - Container configuration
* @returns {JSX.Element} Container wrapper element
*/
export default function Container({
children,
margin = 'md',
maxWidth = 'lg'
}: ContainerProps): JSX.Element {
const marginClasses = {
sm: 'mx-2 my-2',
md: 'mx-4 my-4',
lg: 'mx-8 my-8',
xl: 'mx-16 my-16'
};
const maxWidthClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-4xl',
xl: 'max-w-6xl',
full: 'max-w-full'
};
return (
<div className={`${marginClasses[margin]} ${maxWidthClasses[maxWidth]} mx-auto`}>
{children}
</div>
);
}
React 19 Hooks and Patterns
import { use, useActionState, useOptimistic } from 'react';
/**
* Modern React 19 component with new hooks
* @description Demonstrates React 19 patterns and concurrent features
* @returns {JSX.Element} Interactive form component
*/
export default function ModernFormComponent(): JSX.Element {
// React 19 useActionState hook for form handling
const [state, formAction] = useActionState(
async (prevState, formData) => {
const result = await submitFormAction(formData);
return result;
},
{ status: 'idle', message: '' }
);
// Optimistic updates for better UX
const [optimisticItems, addOptimistic] = useOptimistic(
items,
(state, newItem) => [...state, newItem]
);
return (
<form action={formAction}>
{/* Form fields */}
<button type="submit" disabled={state.status === 'pending'}>
{state.status === 'pending' ? 'Saving...' : 'Save'}
</button>
{state.message && (
<div className="mt-2 text-sm text-red-600">
{state.message}
</div>
)}
</form>
);
}
Next.js 15.2 App Router
File-based Routing Structure
src/app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
├── account/
│ ├── layout.tsx # Account layout
│ ├── page.tsx # Account overview
│ ├── [id]/
│ │ ├── page.tsx # Individual account
│ │ └── transactions/
│ │ └── page.tsx # Account transactions
│ └── settings/
│ └── page.tsx # Account settings
└── api/
├── auth/
│ └── route.ts # Authentication API
├── accounts/
│ ├── route.ts # Accounts API
│ └── [id]/
│ └── route.ts # Individual account API
└── revalidate/
└── route.ts # Cache revalidation API
Layout Patterns
// Root Layout
interface RootLayoutProps {
children: ReactNode;
}
/**
* Root layout component
* @description Provides global HTML structure and styling
* @param {RootLayoutProps} props - Layout props
* @returns {JSX.Element} HTML document structure
*/
export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
return (
<html lang="en">
<body>
<GlobalHeader />
<main>{children}</main>
<GlobalFooter />
</body>
</html>
);
}
Dynamic Routes and Parameters
interface PageProps {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
}
/**
* Dynamic route page component
* @description Handles dynamic routing with parameters
* @param {PageProps} props - Page parameters and search params
* @returns {Promise<JSX.Element>} Dynamic page content
*/
export default async function DynamicPage({
params,
searchParams
}: PageProps): Promise<JSX.Element> {
const data = await fetchPageData(params.id, searchParams);
return (
<div>
<h1>Dynamic Page: {params.id}</h1>
{/* Dynamic content based on parameters */}
</div>
);
}
Parallel and Intercepting Routes
// Layout with parallel routes
interface LayoutProps {
children: ReactNode;
sidebar: ReactNode;
modal: ReactNode;
}
/**
* Parallel routes layout
* @description Manages multiple route segments simultaneously
* @param {LayoutProps} props - Layout children and parallel segments
* @returns {JSX.Element} Parallel routes layout
*/
export default function ParallelLayout({
children,
sidebar,
modal
}: LayoutProps): JSX.Element {
return (
<div className="flex">
<aside className="w-64">{sidebar}</aside>
<main className="flex-1">{children}</main>
{modal}
</div>
);
}
Data Fetching Strategies
Server Components Data Fetching
/**
* Server component with data fetching
* @description Fetches data on the server for optimal performance
* @returns {Promise<JSX.Element>} Server-rendered component
*/
export default async function ServerDataComponent(): Promise<JSX.Element> {
// Parallel data fetching
const [userData, accountData, transactionData] = await Promise.all([
fetchUserData(),
fetchAccountData(),
fetchTransactionData()
]);
return (
<div>
<UserProfile data={userData} />
<AccountSummary data={accountData} />
<TransactionHistory data={transactionData} />
</div>
);
}
Client Components with Streaming
'use client';
import { Suspense } from 'react';
/**
* Client component with streaming data
* @description Implements progressive data loading
* @returns {JSX.Element} Streaming data component
*/
export default function StreamingDataComponent(): JSX.Element {
return (
<div>
<h1>Account Dashboard</h1>
<Suspense fallback={<AccountSkeleton />}>
<AccountBalance />
</Suspense>
<Suspense fallback={<PositionsSkeleton />}>
<PositionsTable />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
API Routes for Data Fetching
// app/api/accounts/route.ts
import { NextRequest, NextResponse } from 'next/server';
/**
* Accounts API route handler
* @description Handles account data requests with caching
* @param {NextRequest} request - Incoming HTTP request
* @returns {Promise<NextResponse>} API response with account data
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
try {
const { searchParams } = new URL(request.url);
const userId = searchParams.get('userId');
if (!userId) {
return NextResponse.json(
{ error: 'User ID is required' },
{ status: 400 }
);
}
// Fetch with caching
const accounts = await fetch(`${process.env.API_BASE_URL}/accounts/${userId}`, {
next: {
revalidate: 300, // Cache for 5 minutes
tags: [`accounts-${userId}`]
}
});
const accountData = await accounts.json();
return NextResponse.json(accountData, {
headers: {
'Cache-Control': 'public, max-age=300, stale-while-revalidate=60'
}
});
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch accounts' },
{ status: 500 }
);
}
}
Caching Implementation
Next.js Configuration
Our Next.js applications are configured with experimental caching enabled:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
useCache: true, // Enable Next.js experimental caching
},
reactStrictMode: true,
trailingSlash: false,
// Additional configuration...
};
module.exports = nextConfig;
Fetch Caching with Tags
/**
* Fetch with cache tags for granular revalidation
* @description Implements tagged caching for selective invalidation
* @param {string} userId - User identifier for cache tagging
* @returns {Promise<UserData>} Cached user data
*/
async function fetchUserDataWithCaching(userId: string): Promise<UserData> {
const response = await fetch(`/api/users/${userId}`, {
next: {
revalidate: 3600, // Cache for 1 hour
tags: [`user-${userId}`, 'users', 'profile-data']
}
});
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
return response.json();
}
Cache Revalidation API
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
/**
* Cache revalidation endpoint
* @description Handles selective cache invalidation
* @param {NextRequest} request - Revalidation request
* @returns {Promise<NextResponse>} Revalidation response
*/
export async function POST(request: NextRequest): Promise<NextResponse> {
try {
const { searchParams } = new URL(request.url);
const tag = searchParams.get('tag');
const path = searchParams.get('path');
const secret = searchParams.get('secret');
// Verify revalidation secret
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json(
{ message: 'Invalid secret' },
{ status: 401 }
);
}
if (tag) {
revalidateTag(tag);
return NextResponse.json({
message: `Revalidated tag: ${tag}`,
revalidated: true
});
}
if (path) {
revalidatePath(path);
return NextResponse.json({
message: `Revalidated path: ${path}`,
revalidated: true
});
}
return NextResponse.json(
{ message: 'Missing tag or path parameter' },
{ status: 400 }
);
} catch (error) {
return NextResponse.json(
{ message: 'Error revalidating cache' },
{ status: 500 }
);
}
}
Browser Cache Headers
/**
* API route with optimized cache headers
* @description Implements browser and CDN caching strategies
* @param {NextRequest} request - API request
* @returns {Promise<NextResponse>} Response with cache headers
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
const data = await fetchStaticData();
return NextResponse.json(data, {
headers: {
// Cache for 1 hour, stale-while-revalidate for 24 hours
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
// ETag for conditional requests
'ETag': `"${generateETag(data)}"`,
// Vary header for content negotiation
'Vary': 'Accept-Encoding, Accept',
}
});
}
Static Asset Caching
// next.config.js cache headers for static assets
const nextConfig = {
async headers() {
return [
{
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}
]
},
{
source: '/fonts/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}
]
}
];
}
};
Performance Best Practices
Code Splitting and Dynamic Imports
import { lazy, Suspense } from 'react';
// Dynamic component loading
const HeavyComponent = lazy(() => import('../components/HeavyComponent'));
const ChartComponent = lazy(() => import('../components/ChartComponent'));
/**
* Performance-optimized page with code splitting
* @description Implements lazy loading for better performance
* @returns {JSX.Element} Optimized page component
*/
export default function OptimizedPage(): JSX.Element {
return (
<div>
<h1>Performance Optimized Page</h1>
{/* Immediately visible content */}
<QuickStats />
{/* Lazy-loaded heavy components */}
<Suspense fallback={<ComponentSkeleton />}>
<HeavyComponent />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<ChartComponent />
</Suspense>
</div>
);
}
Image Optimization
import Image from 'next/image';
/**
* Optimized image component
* @description Implements Next.js image optimization
* @param {ImageProps} props - Image configuration
* @returns {JSX.Element} Optimized image element
*/
interface OptimizedImageProps {
src: string;
alt: string;
width: number;
height: number;
priority?: boolean;
}
export default function OptimizedImage({
src,
alt,
width,
height,
priority = false
}: OptimizedImageProps): JSX.Element {
return (
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
);
}
Bundle Analysis and Optimization
// next.config.js bundle analyzer configuration
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true'
});
const nextConfig = {
// Optimize package imports
optimizePackageImports: [
'lucide-react',
'date-fns',
'lodash-es',
'@adobe/react-spectrum'
],
// Transpile packages for optimization
transpilePackages: [
'@adobe/react-spectrum',
'@react-spectrum/actiongroup',
'@react-spectrum/button'
]
};
module.exports = withBundleAnalyzer(nextConfig);
Memory and Resource Management
import { useEffect, useRef, useCallback } from 'react';
/**
* Resource-optimized component with cleanup
* @description Implements proper resource management and cleanup
* @returns {JSX.Element} Memory-efficient component
*/
export default function ResourceOptimizedComponent(): JSX.Element {
const intervalRef = useRef<NodeJS.Timeout>();
const abortControllerRef = useRef<AbortController>();
const fetchData = useCallback(async () => {
// Cancel previous request if still pending
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
try {
const response = await fetch('/api/data', {
signal: abortControllerRef.current.signal
});
const data = await response.json();
// Handle data...
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
}
}, []);
useEffect(() => {
// Set up polling with cleanup
intervalRef.current = setInterval(fetchData, 30000);
return () => {
// Cleanup on unmount
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [fetchData]);
return (
<div>
{/* Component content */}
</div>
);
}
This comprehensive guide covers the essential patterns and best practices for working with React 19 and Next.js 15.2 in our monorepo. For more detailed information on specific topics, refer to the Architecture Caching documentation.