#Introduction: Why React Still Dominates in 2026
React, released by Meta in 2013, has continuously evolved to meet the demands of modern web development. In 2026, React 19+ stands as the industry standard for building scalable, high-performance user interfaces. With the stabilization of React Server Components (RSC), the Compiler (formerly React Forget), and the Actions API, the ecosystem has matured significantly.
This guide walks you through everything you need to build production-ready React applications in 2026 — from foundational concepts to advanced patterns, with code examples and real-world architecture.
According to the 2025 State of JavaScript survey, React remains the most widely used frontend library at 82% usage among respondents, followed by Vue.js at 46% and Svelte at 23% [1]. Its longevity is not accidental — React's composable model, massive ecosystem, and Meta's continued investment have kept it the default choice for teams of all sizes.
#What Makes React in 2026 Different
- Server Components are stable — no longer experimental, RSC is the default rendering model in Next.js 13+ and Remix
- The React Compiler eliminates manual memoization —
useMemoanduseCallbackare largely unnecessary now - Actions API unifies client and server mutations — forms and data mutations work seamlessly across boundaries
- Concurrent rendering is the default —
startTransition,useDeferredValue, and Suspense compose naturally
#React 19+ — What Changed and Why It Matters
React 19 was a landmark release that shipped several long-awaited features. Here is a quick summary of what moved from experimental to stable:
Feature | Status in React 18 | Status in React 19+ |
|---|---|---|
Server Components | Experimental | Stable |
Actions API | Not available | Stable |
| Not available | Stable |
| Not available | Stable |
React Compiler | Not available | Opt-in stable |
Asset loading | Manual | Built-in preloading |
Document metadata | Manual ( | Built-in |
#Installing React 19+
npm install react@latest react-dom@latest# TypeScript types
npm install --save-dev @types/react@latest @types/react-dom@latestCaption: Always install
reactandreact-domtogether at the same version. Mismatched versions cause subtle runtime errors that are difficult to debug.
#The New use() Hook
The use() hook is one of the most powerful additions in React 19. Unlike other hooks, use() can be called conditionally — including inside loops and if blocks.
import { use, Suspense } from 'react';
// A Promise passed as a prop to a Client Component
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
// use() unwraps the promise — suspends until resolved
const user = use(userPromise);
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
function App() {
const userPromise = fetchUser(1); // Promise created outside
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
Caption:
use()integrates with Suspense automatically. When the promise is pending, React renders the nearest<Suspense>fallback. When it resolves, the component re-renders with the value. Crucially, the Promise must be created outside the component to avoid re-creation on every render.
use() also works with React Context:
import { use } from 'react';
import { ThemeContext } from './ThemeContext';
// Can be called conditionally — unlike useContext
function Button({ isPrimary }: { isPrimary: boolean }) {
if (isPrimary) {
const theme = use(ThemeContext); // Conditional use() — valid!
return <button style={{ background: theme.primary }}>Primary</button>;
}
return <button>Secondary</button>;
}
#Hooks: The Complete Modern Reference
#Core Hooks — Still the Foundation
#useState with TypeScript Best Practices
import { useState } from 'react';
// ✅ Explicit type parameter for non-trivial types
const [user, setUser] = useState<User | null>(null);
// ✅ Lazy initializer for expensive computations
const [data, setData] = useState<ProcessedData>(() => {
return expensiveInitialComputation(); // Runs once, not on every render
});
// ✅ Functional update for state that depends on previous value
setUser(prev => prev ? { ...prev, name: 'Updated' } : null);
Caption: Use the lazy initializer form (
useState(() => ...)) whenever the initial state requires computation, array filtering, or parsing. React calls it only on the initial render.
#useReducer for Complex State
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: { id: string } }
| { type: 'CLEAR_CART' };
interface CartState {
items: CartItem[];
total: number;
}
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM':
return {
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
case 'REMOVE_ITEM':
const item = state.items.find(i => i.id === action.payload.id);
return {
items: state.items.filter(i => i.id !== action.payload.id),
total: state.total - (item?.price ?? 0),
};
case 'CLEAR_CART':
return { items: [], total: 0 };
}
}
function Cart() {
const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
return (
<div>
{cart.items.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: { id: item.id } })}>
Remove
</button>
</div>
))}
<p>Total: ${cart.total.toFixed(2)}</p>
</div>
);
}
Caption:
useReducershines when multiple pieces of state update together, or when next state depends on previous state in complex ways. The discriminated union type for actions gives you exhaustive type checking across every case.
#useEffect in 2026 — What the Compiler Changes
With the React Compiler enabled, the rules for useEffect are simpler: just describe what you want, and the compiler handles dependency arrays.
// Without compiler — manual deps required
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Must manually list userId
// With compiler enabled — compiler infers deps automatically
useEffect(() => {
fetchUser(userId).then(setUser);
}); // Compiler adds correct deps at build time
Without the compiler, follow these rules:
// ✅ CORRECT — all reactive values in dep array
const [query, setQuery] = useState('');
const [results, setResults] = useState<Result[]>([]);
useEffect(() => {
if (!query) return;
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
// Cleanup: cancel in-flight request on re-run or unmount
return () => controller.abort();
}, [query]); // query is reactive — belongs in deps
Caption: Always return a cleanup function from
useEffectwhen you create subscriptions, timers, or async operations. The cleanup runs before the next effect and on unmount, preventing stale state updates and memory leaks.
#useOptimistic — The New Pattern for Optimistic UI
useOptimistic is the idiomatic React 19 way to implement optimistic updates without external libraries.
import { useOptimistic, useTransition } from 'react';
type Todo = { id: string; text: string; done: boolean };
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos, newTodo: Todo) => [...currentTodos, newTodo]
);
const [isPending, startTransition] = useTransition();
async function handleAdd(text: string) {
const optimistic: Todo = { id: crypto.randomUUID(), text, done: false };
startTransition(async () => {
addOptimisticTodo(optimistic); // Instantly shows the new todo
await createTodo(text); // Server call happens in background
});
}
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.done ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
);
}
Caption:
useOptimisticapplies an optimistic state immediately, then automatically reverts if the async operation throws. This replaces manual patterns involving local state copies and error rollback logic.
#Custom Hooks: Building Composable Abstractions
Custom hooks are how you share stateful logic across components. Here is a production-grade useFetch hook:
import { useState, useEffect, useRef, useCallback } from 'react';
interface FetchState<T> {
data: T | null;
error: Error | null;
loading: boolean;
refetch: () => void;
}
function useFetch<T>(url: string): FetchState<T> {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
const [trigger, setTrigger] = useState(0);
const refetch = useCallback(() => setTrigger(t => t + 1), []);
useEffect(() => {
let cancelled = false;
const controller = new AbortController();
setLoading(true);
setError(null);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
})
.then(result => {
if (!cancelled) {
setData(result);
setLoading(false);
}
})
.catch(err => {
if (!cancelled && err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
controller.abort();
};
}, [url, trigger]);
return { data, error, loading, refetch };
}
// Usage
function ProductPage({ id }: { id: string }) {
const { data, loading, error, refetch } = useFetch<Product>(`/api/products/${id}`);
if (loading) return <Skeleton />;
if (error) return <ErrorBanner error={error} onRetry={refetch} />;
return <ProductDetail product={data!} />;
}
Caption: The
cancelledflag prevents state updates on unmounted components — a common source of React warnings. TheAbortControllercancels in-flight network requests when the component unmounts or the URL changes.
#React Server Components (RSC) Deep Dive
React Server Components are arguably the most significant architectural shift since hooks. They allow components to run exclusively on the server, with zero JavaScript shipped to the client.
#The Mental Model
┌─────────────────────────────────────────┐
│ Server │
│ ┌───────────────────────────────────┐ │
│ │ Server Components │ │
│ │ - Access DB, filesystem, secrets│ │
│ │ - No useState, no useEffect │ │
│ │ - Zero JS bundle impact │ │
│ └──────────────┬────────────────────┘ │
│ │ Renders HTML + RSC │
│ │ payload │
└─────────────────┼───────────────────────┘
│
┌─────────────────┼───────────────────────┐
│ Client │
│ ┌────────────────────────────────────┐ │
│ │ Client Components ('use client') │ │
│ │ - useState, useEffect, etc. │ │
│ │ - Event handlers │ │
│ │ - Browser APIs │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
Caption: Server Components and Client Components are rendered in fundamentally different environments. The boundary is explicit: any file with
'use client'at the top is a Client Component. Everything else defaults to Server Component in RSC-enabled frameworks.
#A Realistic Server Component
// app/dashboard/page.tsx — Server Component (no 'use client')
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { MetricsGrid } from './MetricsGrid'; // Server Component
import { RealtimeChart } from './RealtimeChart'; // Client Component
// Direct DB access — no API layer needed
async function getDashboardData(userId: string) {
const [metrics, recentOrders] = await Promise.all([
db.metrics.findMany({ where: { userId }, orderBy: { date: 'desc' }, take: 30 }),
db.orders.findMany({ where: { userId }, take: 10, include: { items: true } }),
]);
return { metrics, recentOrders };
}
export default async function DashboardPage() {
const session = await auth(); // Server-only auth check
if (!session) redirect('/login');
const { metrics, recentOrders } = await getDashboardData(session.user.id);
return (
<main>
<h1>Dashboard</h1>
{/* Server Component — renders the grid with zero JS */}
<MetricsGrid metrics={metrics} />
{/* Client Component — receives serializable props from server */}
<RealtimeChart initialData={metrics} userId={session.user.id} />
</main>
);
}
Caption: Notice that the database query runs directly in the component — no
useEffect, no API call, no loading state. The entire data-fetching and rendering pipeline is server-side. OnlyRealtimeChart, which needs WebSockets, is a Client Component.
#Client Components
// components/RealtimeChart.tsx
'use client'; // This directive marks the Client Component boundary
import { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer } from 'recharts';
interface Props {
initialData: Metric[];
userId: string;
}
export function RealtimeChart({ initialData, userId }: Props) {
const [data, setData] = useState(initialData);
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/metrics/${userId}`);
ws.onmessage = (event) => {
const newPoint: Metric = JSON.parse(event.data);
setData(prev => [...prev.slice(-29), newPoint]);
};
return () => ws.close();
}, [userId]);
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<XAxis dataKey="date" />
<YAxis />
<Line type="monotone" dataKey="value" stroke="#0A4F5C" />
</LineChart>
</ResponsiveContainer>
);
}
Caption: Server Components can pass serializable data (plain objects, arrays, primitives) as props to Client Components. They cannot pass functions, class instances, or non-serializable values across the boundary.
#Composing Server and Client Components
// ✅ Correct — Client Component as a child (slot pattern)
// Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<ServerSidebar /> {/* Server Component */}
<ClientShell> {/* Client Component */}
{children} {/* Server-rendered content passed as children */}
</ClientShell>
</div>
);
}
// ❌ Wrong — importing Server Component inside Client Component
'use client';
import { ServerComponent } from './ServerComponent'; // ❌ This will be treated as a Client Component
Caption: The most common RSC mistake is importing a Server Component into a Client Component. Instead, pass Server Components as
childrenor as named props. This preserves the server/client boundary and avoids sending server code to the browser.
#The React Compiler (Formerly React Forget)
The React Compiler is a build-time tool that automatically memoizes components and hooks. In 2026, it is stable and available as an opt-in Babel/SWC plugin.
#Setup
npm install --save-dev babel-plugin-react-compiler// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
target: '18', // or '19'
sources: (filename) => filename.includes('/src/'), // Only compile src files
}]
]
};
#What the Compiler Does
Before the compiler, you had to manually memoize:
// Before compiler — manual memoization required
import { useMemo, useCallback, memo } from 'react';
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }: Props) {
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleSelect = useCallback(
(id: string) => onSelect(id),
[onSelect]
);
return (
<ul>
{sorted.map(item => (
<li key={item.id} onClick={() => handleSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
After the compiler, write idiomatic code and let the compiler optimize:
// After compiler — write naturally, compiler adds memoization
function ExpensiveList({ items, onSelect }: Props) {
// Compiler automatically detects this as an expensive computation
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{sorted.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
Caption: The compiler statically analyzes your component code and inserts the equivalent of
useMemo,useCallback, andmemowhere it determines values are stable between renders. This eliminates an entire class of performance bugs caused by incorrect or missing dependency arrays.
#When to Still Use Manual Memoization
Even with the compiler, some cases require manual control:
// Preserve referential identity across renders for external subscriptions
const stableCallback = useCallback(
() => externalLibrary.subscribe(handleEvent),
[] // Explicitly empty — we want this reference to never change
);
// Very expensive computation with large input — be explicit for clarity
const processedData = useMemo(
() => runMillisecondComputation(rawData),
[rawData]
);
#6. Concurrent Features: Transitions, Suspense & Deferred Values
#useTransition — Keeping the UI Responsive
useTransition marks state updates as non-urgent, allowing React to interrupt them if the user interacts.
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Product[]>([]);
const [isPending, startTransition] = useTransition();
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setQuery(value); // Urgent — update the input immediately
startTransition(() => {
// Non-urgent — can be interrupted if user keeps typing
const filtered = searchProducts(value); // Can be expensive
setResults(filtered);
});
}
return (
<div>
<input value={query} onChange={handleSearch} placeholder="Search..." />
{isPending && <Spinner />}
<ProductGrid products={results} />
</div>
);
}
Caption: Without
useTransition, a slowsetResultscomputation would block the input from updating until it completes, making the UI feel laggy. With transitions, React prioritizes the input update and runs the expensive result computation in the background.
#Suspense with Data Fetching
import { Suspense } from 'react';
// Each Suspense boundary independently shows its fallback
export default function ProductsPage() {
return (
<div>
<h1>Products</h1>
{/* Shows skeleton while categories load */}
<Suspense fallback={<CategorySkeleton />}>
<CategoryNav />
</Suspense>
{/* Shows skeleton while featured products load — independent of above */}
<Suspense fallback={<ProductGridSkeleton />}>
<FeaturedProducts />
</Suspense>
{/* Nested Suspense — loads after parent resolves */}
<Suspense fallback={<RecommendationSkeleton />}>
<Suspense fallback={<UserSkeleton />}>
<UserInfo />
</Suspense>
<Recommendations />
</Suspense>
</div>
);
}
Caption: Independent Suspense boundaries allow different parts of the page to load in parallel. If
CategoryNavandFeaturedProductsboth fetch data, their loading states are isolated — one does not block the other.
#useDeferredValue — Debouncing Without Timers
import { useState, useDeferredValue } from 'react';
function SearchResults({ query }: { query: string }) {
// Expensive filtering — runs with the deferred (stale) value
const results = filterProducts(query);
return <ProductList products={results} />;
}
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // Lags behind query during transitions
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</div>
);
}
Caption:
useDeferredValueis similar to debouncing, but instead of a time delay, it defers to React's scheduler. During a high-priority update (like typing), the deferred value keeps its old value, preventing the expensiveSearchResultsfrom re-rendering until the browser is idle.
#The Actions API: Server Mutations Made Simple
The Actions API, stable in React 19, provides a standard way to handle form submissions and server mutations without manual fetch calls or complex state management.
#Server Actions
// app/actions/products.ts
'use server'; // This file's exports become server actions
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const CreateProductSchema = z.object({
name: z.string().min(1).max(100),
price: z.number().positive(),
description: z.string().optional(),
});
export async function createProduct(formData: FormData) {
const raw = {
name: formData.get('name'),
price: Number(formData.get('price')),
description: formData.get('description') || undefined,
};
const result = CreateProductSchema.safeParse(raw);
if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}
const product = await db.products.create({ data: result.data });
revalidatePath('/products'); // Invalidate cached product pages
return { success: true, product };
}
Caption: Server Actions run exclusively on the server but are called from Client Components like regular functions. They have access to environment variables, databases, and secrets. Zod validation ensures type safety at the boundary.
#Using Actions in Forms
'use client';
import { useActionState } from 'react';
import { createProduct } from '@/app/actions/products';
type ActionState = {
error?: Record<string, string[]>;
success?: boolean;
};
export function CreateProductForm() {
const [state, action, isPending] = useActionState<ActionState, FormData>(
createProduct,
{}
);
return (
<form action={action}>
<div>
<label htmlFor="name">Product Name</label>
<input id="name" name="name" type="text" required />
{state.error?.name && (
<span className="error">{state.error.name[0]}</span>
)}
</div>
<div>
<label htmlFor="price">Price</label>
<input id="price" name="price" type="number" step="0.01" required />
{state.error?.price && (
<span className="error">{state.error.price[0]}</span>
)}
</div>
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Product'}
</button>
{state.success && <p>Product created successfully!</p>}
</form>
);
}
Caption:
useActionState(formerlyuseFormStatein React 18) manages the full lifecycle of a form action: pending state, response data, and error handling — all in one hook. This replaces patterns that previously requireduseState+useEffect+ manual fetch logic.
#8. State Management in 2026
#When to Use What
Use Case | Recommended Solution |
|---|---|
Local UI state (modals, toggles) |
|
Complex local state |
|
Shared state, low frequency updates | React Context |
Global state, high frequency updates | Zustand or Jotai |
Server/async state | TanStack Query |
Form state | React Hook Form |
#Zustand — Minimal Global State
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { persist } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
totalPrice: () => number;
}
const useCartStore = create<CartStore>()(
persist(
immer((set, get) => ({
items: [],
addItem: (item) => set(state => {
const existing = state.items.find(i => i.id === item.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...item, quantity: 1 });
}
}),
removeItem: (id) => set(state => {
state.items = state.items.filter(i => i.id !== id);
}),
clearCart: () => set({ items: [] }),
totalPrice: () =>
get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
})),
{ name: 'cart-storage' } // Persists to localStorage automatically
)
);
// Usage in component
function CartButton() {
const { items, addItem } = useCartStore();
return <button onClick={() => addItem(product)}>{items.length} items</button>;
}
Caption: Zustand with Immer middleware allows you to write "mutating" logic that is actually applied immutably under the hood. The
persistmiddleware automatically syncs state tolocalStoragewith no additional setup.
#TanStack Query v5 for Server State
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Query — declarative data fetching with caching
function ProductList() {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json()),
staleTime: 1000 * 60 * 5, // Data is fresh for 5 minutes
});
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <Grid products={data} />;
}
// Mutation with cache invalidation
function AddProductButton({ product }: { product: NewProduct }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: NewProduct) =>
fetch('/api/products', { method: 'POST', body: JSON.stringify(data) }).then(r => r.json()),
onSuccess: () => {
// Invalidate and refetch products list
queryClient.invalidateQueries({ queryKey: ['products'] });
},
onMutate: async (newProduct) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['products'] });
// Snapshot current value
const previous = queryClient.getQueryData(['products']);
// Optimistically update
queryClient.setQueryData(['products'], (old: Product[]) => [...old, newProduct]);
return { previous };
},
onError: (err, newProduct, context) => {
// Rollback on error
queryClient.setQueryData(['products'], context?.previous);
},
});
return (
<button onClick={() => mutation.mutate(product)} disabled={mutation.isPending}>
{mutation.isPending ? 'Adding...' : 'Add Product'}
</button>
);
}
Caption: TanStack Query separates "server state" (data that lives on the server and needs to be kept in sync) from "UI state" (data that only exists on the client). This mental separation dramatically reduces component complexity.
#Performance Patterns & Optimization
#Code Splitting with React.lazy
import { lazy, Suspense } from 'react';
// Component is loaded only when first rendered
const HeavyDashboard = lazy(() => import('./HeavyDashboard'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));
function App() {
const [showDashboard, setShowDashboard] = useState(false);
return (
<div>
<button onClick={() => setShowDashboard(true)}>Open Dashboard</button>
{showDashboard && (
<Suspense fallback={<DashboardSkeleton />}>
<HeavyDashboard />
</Suspense>
)}
</div>
);
}
Caption:
React.lazyenables route-level and component-level code splitting. The bundle forHeavyDashboardis only downloaded when the user triggers it. Combine withSuspensefor a seamless loading experience.
#Virtualization for Long Lists
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
function VirtualList({ items }: { items: Product[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80, // Estimated row height in px
overscan: 5, // Render 5 extra items above/below viewport
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ProductRow product={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
Caption: Without virtualization, rendering a list of 10,000 items creates 10,000 DOM nodes. With
@tanstack/react-virtual, only the ~10 items visible in the viewport are rendered at any time, regardless of list size.
#Core Web Vitals Targets for 2026
Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
LCP (Largest Contentful Paint) | < 2.5s | 2.5s – 4s | > 4s |
FID / INP (Interaction to Next Paint) | < 200ms | 200ms – 500ms | > 500ms |
CLS (Cumulative Layout Shift) | < 0.1 | 0.1 – 0.25 | > 0.25 |
TTFB (Time to First Byte) | < 800ms | 800ms – 1.8s | > 1.8s |
Caption: In 2026, Google's ranking algorithm heavily weights Core Web Vitals. INP (Interaction to Next Paint) replaced FID in March 2024 and measures the responsiveness of all interactions throughout the page lifecycle, not just the first [2].
#10. Architecture Patterns for Production Apps
#Feature-Based Folder Structure
src/
├── app/ # Next.js App Router pages
│ ├── (auth)/
│ │ ├── login/
│ │ └── register/
│ ├── dashboard/
│ └── products/
│ ├── [id]/
│ └── page.tsx
├── features/ # Feature modules
│ ├── auth/
│ │ ├── components/ # Feature-specific components
│ │ ├── hooks/ # Feature-specific hooks
│ │ ├── actions/ # Server actions
│ │ ├── store.ts # Zustand slice
│ │ └── types.ts
│ ├── products/
│ └── cart/
├── shared/ # Shared across features
│ ├── components/ # Generic UI components
│ ├── hooks/ # Generic hooks
│ ├── lib/ # Third-party lib configs
│ └── types/ # Global types
└── styles/
Caption: Feature-based folder structure co-locates everything related to a feature in one place. This makes it easy to delete a feature entirely (delete one folder), trace data flow, and onboard new developers who can reason about bounded areas of the codebase.
#Error Boundaries
'use client';
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: (error: Error, reset: () => void) => ReactNode;
}
interface State {
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
state: State = { error: null };
static getDerivedStateFromError(error: Error): State {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
// Log to error reporting service
console.error('Error boundary caught:', error, info.componentStack);
reportError({ error, componentStack: info.componentStack });
}
reset = () => this.setState({ error: null });
render() {
if (this.state.error) {
return this.props.fallback
? this.props.fallback(this.state.error, this.reset)
: <DefaultErrorUI error={this.state.error} onReset={this.reset} />;
}
return this.props.children;
}
}
// Usage with render prop
function Dashboard() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={reset}>Try Again</button>
</div>
)}
>
<DashboardContent />
</ErrorBoundary>
);
}
Caption: Error Boundaries must be class components — React has not provided a hook equivalent. Place Error Boundaries strategically to isolate failures: one per major section, not one at the root. If the root crashes, users see nothing.
#11. Testing React Applications in 2026
#Testing Philosophy: The Testing Trophy
/\
/ \ E2E (Playwright) — 10%
/ \ — Critical user journeys
/------\
/ \ Integration (Vitest + RTL) — 60%
/ \ — Component + hook behavior
/------------\
/ \ Unit (Vitest) — 30%
/________________\ — Pure functions, utils, reducers
Caption: The Testing Trophy (popularized by Kent C. Dodds) emphasizes integration tests over unit tests for UI code. Test what users do, not implementation details like component state or internal methods [3].
#Vitest + React Testing Library
// products/ProductCard.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { ProductCard } from './ProductCard';
const mockProduct: Product = {
id: '1',
name: 'Wireless Keyboard',
price: 79.99,
inStock: true,
image: '/keyboard.jpg',
};
describe('ProductCard', () => {
it('renders product information', () => {
render(<ProductCard product={mockProduct} onAddToCart={() => {}} />);
expect(screen.getByText('Wireless Keyboard')).toBeInTheDocument();
expect(screen.getByText('$79.99')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /add to cart/i })).toBeEnabled();
});
it('calls onAddToCart when button is clicked', async () => {
const user = userEvent.setup();
const handleAddToCart = vi.fn();
render(<ProductCard product={mockProduct} onAddToCart={handleAddToCart} />);
await user.click(screen.getByRole('button', { name: /add to cart/i }));
expect(handleAddToCart).toHaveBeenCalledOnce();
expect(handleAddToCart).toHaveBeenCalledWith(mockProduct);
});
it('disables add to cart when out of stock', () => {
render(
<ProductCard
product={{ ...mockProduct, inStock: false }}
onAddToCart={() => {}}
/>
);
expect(screen.getByRole('button', { name: /out of stock/i })).toBeDisabled();
});
});
Caption: Use
userEventoverfireEventfor realistic interaction simulation.userEvent.clickdispatches the full sequence of pointer events (pointerdown, mousedown, click) that real users generate, catching bugs thatfireEvent.clickmisses.
#Testing Custom Hooks
// hooks/useCart.test.ts
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { useCart } from './useCart';
describe('useCart', () => {
it('adds items and calculates total correctly', () => {
const { result } = renderHook(() => useCart());
act(() => {
result.current.addItem({ id: '1', name: 'Widget', price: 10.00 });
result.current.addItem({ id: '2', name: 'Gadget', price: 25.00 });
});
expect(result.current.items).toHaveLength(2);
expect(result.current.total).toBe(35.00);
});
});
#Playwright E2E Tests
// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Checkout flow', () => {
test('completes a purchase successfully', async ({ page }) => {
// Navigate and add product
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).first().click();
// Go to cart
await page.getByRole('link', { name: /cart/i }).click();
await expect(page.getByText('1 item')).toBeVisible();
// Checkout
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Card Number').fill('4242424242424242');
await page.getByRole('button', { name: 'Pay Now' }).click();
// Confirm
await expect(page.getByText('Order confirmed')).toBeVisible({ timeout: 10000 });
});
});
Caption: Playwright E2E tests run in real browsers (Chromium, Firefox, WebKit) against your deployed application. Use them for critical user journeys — login, checkout, key data entry flows — not for every component interaction.
#12. The Ecosystem: Next.js, Remix, TanStack & More
#Framework Comparison in 2026
Framework | Rendering Model | Best For | RSC Support |
|---|---|---|---|
Next.js 15+ | App Router + RSC | Full-stack apps, e-commerce | ✅ Native |
Remix v3 | Nested routes, loaders | Content-heavy apps | ✅ Native |
Vite + React | SPA / CSR | Admin dashboards, internal tools | ❌ Client only |
Astro | Islands architecture | Marketing, content sites | ✅ Via integration |
TanStack Start | SSR + RSC | Highly type-safe apps | ✅ Experimental |
#Next.js 15 App Router — Key Patterns
// app/products/[id]/page.tsx
// Generate static paths at build time
export async function generateStaticParams() {
const products = await db.products.findMany({ select: { id: true } });
return products.map(p => ({ id: p.id }));
}
// Metadata API — replaces react-helmet
export async function generateMetadata({ params }: { params: { id: string } }) {
const product = await db.products.findUnique({ where: { id: params.id } });
return {
title: product?.name ?? 'Product Not Found',
description: product?.description,
openGraph: {
images: [product?.image ?? '/og-default.jpg'],
},
};
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.products.findUnique({ where: { id: params.id } });
if (!product) notFound();
return <ProductDetail product={product} />;
}
Caption:
generateStaticParams+ async Server Components replacesgetStaticPaths+getStaticPropsfrom the Pages Router. The mental model is simpler: the component is the data loader.
#13. References
[1] Greif, S., & Benitte, R. (2025). State of JavaScript 2025. Retrieved from https://stateofjs.com
[2] Google Chrome Team. (2024). Interaction to Next Paint (INP) becomes a Core Web Vital. Web.dev. Retrieved from https://web.dev/blog/inp-cwv-launch
[3] Dodds, K. C. (2021). The Testing Trophy and Testing Classifications. kentcdodds.com. Retrieved from https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications
[4] React Team. (2024). React 19 Release Candidate. React Blog. Retrieved from https://react.dev/blog/2024/04/25/react-19
[5] React Team. (2024). React Compiler. react.dev. Retrieved from https://react.dev/learn/react-compiler
[6] Abramov, D., & Meta Open Source. (2023). React Server Components RFC. GitHub. Retrieved from https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md
[7] TanStack. (2025). TanStack Query v5 Documentation. Retrieved from https://tanstack.com/query/latest
[8] Zustand Contributors. (2025). Zustand Documentation. Retrieved from https://docs.pmnd.rs/zustand
[9] Testing Library. (2025). React Testing Library. Retrieved from https://testing-library.com/docs/react-testing-library/intro
[10] Playwright Team. (2025). Playwright Documentation. Retrieved from https://playwright.dev/docs/intro
[11] Next.js Team. (2025). Next.js 15 Documentation. Vercel. Retrieved from https://nextjs.org/docs
Last updated: April 2026. All code examples use React 19+, TypeScript 5.x, and Node.js 22+.
Comments (1)
Just Wow mate this greate work