50 React & Next.js Rules
Essential rules for building production React 19 and Next.js applications. Covers architecture, performance, data fetching, security, and ecosystem best practices.
Fundamentals & Architecture (Rules 1-10)
1. Default to Server Components. In Next.js App Router (and React 19 ecosystems), components are Server Components by default. Only add "use client" when you need interactivity, state, or browser APIs. This minimizes client-side JavaScript and improves load times.
2. Use the App Router exclusively for new projects. The Pages Router is legacy. App Router provides nested layouts, streaming, better data fetching, and full React Server Components support.
3. Master the client/server boundary. Server Components can render Client Components (and pass serializable props). Client Components cannot import Server Components. Never pass non-serializable values (functions, classes) across the boundary.
- Serializable: strings, numbers, booleans, arrays, plain objects,
Date,Map,Set - Not serializable: functions, class instances, DOM nodes, React elements with closures
4. Colocate logic with components. Keep data fetching, styles, and related utilities near the components that use them. Keep side effects predictable and server-first where possible.
app/
dashboard/
page.tsx # Route
dashboard-chart.tsx # Component used only here
actions.ts # Server Actions for this route
loading.tsx # Loading UI
5. Prefer composition over inheritance or complex state. Build small, focused, reusable components. Lift state only when necessary. Favor local state and server-managed data over global stores.
// Good: composition with children
<Card>
<CardHeader>Title</CardHeader>
<CardBody>{content}</CardBody>
</Card>
// Avoid: inheritance or mega-components
class SpecialCard extends Card { ... }6. Adopt TypeScript by default. Use strict typing, generics for components and hooks, and the modern JSX transform. TypeScript catches errors early and improves maintainability in large apps.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}7. Think in Server + Client, not just "components". Design with a mental model of server rendering first, then add client interactivity sparingly. Performance is a design constraint, not an afterthought.
| Layer | Handles | Examples |
|---|---|---|
| Server Components | Data fetching, static content, SEO | Product pages, dashboards, articles |
| Client Components | Interactivity, state, browser APIs | Forms, modals, dropdowns, animations |
8. Use React 19's new hooks wisely. Leverage the new APIs for common patterns:
| Hook | Purpose |
|---|---|
useActionState | Form action state with pending, error, data |
useOptimistic | Optimistic UI updates during async actions |
useFormStatus | Access pending state of parent form |
use() | Unwrap promises and context conditionally |
9. Embrace React Server Components (RSCs) as the foundation. RSCs enable direct backend access, zero client JS for static parts, and better streaming. Avoid forcing everything to client-side.
Benefits of RSCs:
- Direct database/API access without an API layer
- Zero JavaScript shipped for server-only components
- Automatic code splitting at the component level
- Streaming and progressive rendering
10. Follow a production checklist before deploy. Run next build, test with next start, optimize images and fonts, and verify Core Web Vitals.
# Pre-deploy checklist
next build # Check for build errors
next start # Test production mode locally
npx lighthouse # Check performance metricsPerformance & Optimization (Rules 11-25)
11. Eliminate waterfalls in data fetching. Use parallel fetches, Suspense boundaries, and server-side data loading to avoid sequential blocking requests.
// Bad: sequential (waterfall)
const user = await getUser(id);
const posts = await getPosts(user.id);
// Good: parallel
const [user, posts] = await Promise.all([
getUser(id),
getPosts(id),
]);12. Trust the React Compiler (React Forget). In React 19, the compiler automatically memoizes where safe. Write simpler code without excessive useMemo and useCallback unless profiling shows issues.
// Before: manual memoization everywhere
const filtered = useMemo(() => items.filter(predicate), [items, predicate]);
const handleClick = useCallback(() => doThing(id), [id]);
// After (with React Compiler): just write it
const filtered = items.filter(predicate);
const handleClick = () => doThing(id);13. Use Suspense for declarative data fetching and loading states. Wrap dynamic sections with Suspense. Combine with streaming for progressive rendering and better UX.
<Suspense fallback={<Skeleton />}>
<ProductDetails id={id} />
</Suspense>14. Leverage Partial Prerendering (PPR). In Next.js 15+, statically render the shell of a page while streaming dynamic parts. This is the best of both worlds for many applications.
// The static shell renders instantly
// Dynamic parts stream in as they resolve
export default function Page() {
return (
<div>
<Header /> {/* Static */}
<Suspense fallback={<Skeleton />}>
<LiveFeed /> {/* Dynamic, streamed */}
</Suspense>
<Footer /> {/* Static */}
</div>
);
}15. Choose the right rendering strategy.
| Strategy | Best For | Trade-off |
|---|---|---|
| Static (SSG) | Public content, marketing pages | Stale until rebuild |
| ISR | Blog posts, product pages | Stale within revalidation window |
| SSR | User-specific, real-time data | Slower TTFB, more server load |
| CSR | Internal tools, auth-gated dashboards | No SEO, slower initial load |
16. Optimize images aggressively. Use the Next.js Image component with sizes, modern formats (WebP/AVIF), and lazy loading. Compress assets and avoid third-party font/CDN waterfalls.
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
sizes="(max-width: 768px) 100vw, 1200px"
priority // Above the fold? Use priority
/>17. Implement proper caching and revalidation. Use fetch cache options, revalidatePath/revalidateTag, or the use cache directive. Understand the Next.js caching layers:
| Cache Layer | What | Duration |
|---|---|---|
| Request Memoization | Dedupes identical fetches in one render | Single request |
| Data Cache | Caches fetch responses | Until revalidated |
| Full Route Cache | Caches entire rendered pages | Until revalidated |
| Router Cache | Client-side route cache | Session-based |
18. Minimize client bundle size. Keep Client Components small and focused. Use dynamic imports for heavy or conditional client code.
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./chart"), {
loading: () => <Skeleton className="h-64" />,
});19. Avoid unnecessary re-renders. With the Compiler this is less manual, but still profile with React DevTools. Use keys correctly in lists. Do not use array index as key for dynamic lists.
20. Stream and hydrate selectively. Use Suspense boundaries to enable selective hydration and faster perceived performance. Users can interact with hydrated parts while other sections are still loading.
21. Optimize for Core Web Vitals.
| Metric | Target | How |
|---|---|---|
| LCP (Largest Contentful Paint) | Under 2.5s | Priority images, preload fonts, SSR |
| CLS (Cumulative Layout Shift) | Under 0.1 | Set image dimensions, avoid layout shifts |
| INP (Interaction to Next Paint) | Under 200ms | Small client bundles, useTransition |
22. Use Turbopack in development. It is now stable and delivers significantly faster HMR and builds in Next.js 15+.
next dev --turbopack23. Compress and lazy-load non-critical assets. Use dynamic imports for routes and components, modern image formats, and avoid runtime CSS-in-JS overhead where possible (favor Tailwind or CSS Modules).
24. Monitor and profile in production-like environments. Use Next.js built-in metrics, Lighthouse, and tools like Vercel Analytics to catch real-user performance issues.
25. Reduce JavaScript sent to the client. Aim for smaller bundles via RSCs, code splitting, and tree-shaking. Server Components can reduce client JS by 30-50% in many cases.
Data Fetching, State & Forms (Rules 26-35)
26. Fetch data on the server by default. Use Server Components or Route Handlers. Avoid client-side fetching for initial data when possible.
// Server Component: direct data access
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
return <ProductDetails product={product} />;
}27. Use Server Actions for mutations. They simplify forms, handle async transitions, pending states, and errors automatically in React 19 and Next.js. Prefer them over traditional API routes for many use cases.
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await db.post.create({ data: { title } });
revalidatePath("/posts");
}28. Prefer TanStack Query (or SWR) for client-side server state. Combine with server fetching for optimal caching and synchronization. Use client-side fetching only for data that changes frequently after initial load.
29. Handle forms with Actions and optimistic updates. Use useOptimistic and useActionState for smooth UX without manual loading and error boilerplate.
const [optimisticItems, addOptimistic] = useOptimistic(
items,
(state, newItem: Item) => [...state, { ...newItem, pending: true }]
);30. Keep state local unless shared. Avoid overusing global state managers. Lift state only when truly needed.
| State Type | Solution |
|---|---|
| UI state (one component) | useState |
| Complex local state | useReducer |
| Shared across siblings | Lift state to parent |
| Shared across app | Zustand or Context |
| Server data | Server Components, SWR, TanStack Query |
| URL state | useSearchParams |
31. Deduplicate requests automatically. Next.js and React 19 handle many cases through request memoization. Ensure consistent cache keys.
32. Use async/await naturally in Server Components. Data fetching is synchronous during render on the server. Async Server Components are first-class in React 19.
33. Validate inputs on the server. Never trust client data. Use schemas (Zod, etc.) in Server Actions or Route Handlers.
"use server";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
});
export async function createUser(formData: FormData) {
const result = schema.safeParse(Object.fromEntries(formData));
if (!result.success) return { error: result.error.flatten() };
// ... create user
}34. Manage async boundaries with Suspense and error boundaries. Provide graceful fallbacks for loading and errors.
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>35. Avoid direct browser API access in shared code. Check for "use client" and environment when needed. Use typeof window !== "undefined" guards sparingly, and prefer splitting into server and client modules.
Security & Best Practices (Rules 36-45)
36. Never expose secrets to the client. Use server-only environment variables (no NEXT_PUBLIC_ prefix for sensitive data). Proxy API calls through Server Actions or Route Handlers.
# .env
STRIPE_SECRET_KEY=sk_live_... # Server only
NEXT_PUBLIC_STRIPE_KEY=pk_live_... # Safe for client37. Secure Server Actions. Protect against unauthorized calls with proper auth checks. Keep dependencies updated. Server Actions are public HTTP endpoints, so always verify the user.
"use server";
import { auth } from "@/lib/auth";
export async function deletePost(id: string) {
const session = await auth();
if (!session) throw new Error("Unauthorized");
// ... delete post
}38. Implement proper authentication and authorization. Use httpOnly, Secure, SameSite cookies for sessions. Validate on the server. Never rely on client-side checks alone.
39. Sanitize and validate all inputs. Prevent XSS, CSRF (Server Actions have built-in CSRF protection via origin checks), and injection attacks.
40. Follow a production security checklist:
- Update Next.js and React to latest patched versions
- Enable security headers (CSP, X-Frame-Options, etc.)
- Audit dependencies with
npm audit - Never log sensitive data
- Use HTTPS everywhere
41. Use private folders (_folder) in App Router. Keep non-route files (utils, components) out of the routing system.
app/
dashboard/
_components/ # Not a route
_lib/ # Not a route
page.tsx # Route
42. Organize project structure scalably.
src/
app/ # Routes and layouts
components/ # Shared UI components
ui/ # shadcn/ui primitives
lib/ # Utilities, constants, types
hooks/ # Custom hooks
actions/ # Shared Server Actions
43. Write small, single-responsibility components. Easier to test, optimize, and maintain. A component should do one thing well. If you are scrolling to understand it, split it.
44. Test thoroughly. Unit test hooks and components, integration test data flows, and end-to-end test critical paths. Include hydration and streaming scenarios.
| Test Type | Tool | What to Test |
|---|---|---|
| Unit | Vitest | Hooks, utilities, Server Actions |
| Component | React Testing Library | User interactions, rendering |
| E2E | Playwright | Critical user flows, payments |
45. Document and review boundaries. Clearly mark "use client", Server Actions, and data-fetching logic in code reviews. Make the client/server split obvious.
Advanced & Ecosystem (Rules 46-50)
46. Integrate AI features thoughtfully. Proxy calls through your server (never expose API keys client-side). Use Server-Sent Events or the Vercel AI SDK for streaming. Cache aggressively and track costs.
// Server Action with AI SDK
"use server";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function chat(messages: Message[]) {
const result = await streamText({
model: openai("gpt-4o"),
messages,
});
return result.toDataStreamResponse();
}47. Leverage metadata and SEO tools. Use the Next.js metadata API, generate sitemaps and robots.txt dynamically, and ensure server-rendered content for crawlers.
export const metadata: Metadata = {
title: "Product Name",
description: "Product description for SEO",
openGraph: { images: ["/og-image.png"] },
};48. Stay updated but stable. Pin major versions where needed. Test React Compiler and new caching behaviors incrementally. Read changelogs before upgrading.
49. Prioritize accessibility and inclusive design. Use semantic HTML, ARIA where needed, and test with screen readers. Pay special attention to dynamic streaming content that may not announce correctly.
Key accessibility checks:
- All images have meaningful
alttext - Forms have associated labels
- Color contrast meets WCAG AA (4.5:1 for text)
- Keyboard navigation works for all interactive elements
- Focus management on route changes
50. Treat performance and maintainability as ongoing disciplines. Profile regularly, refactor toward server-first patterns, and use community resources. Performance is not a one-time task. It is a continuous practice.
FAQs
When should you add "use client" to a component?
- Only when the component needs interactivity, state (
useState,useReducer), or browser APIs - Server Components are the default in App Router -- do not add
"use client"preemptively - If a component only renders data or static content, keep it as a Server Component
What values can you pass across the server/client boundary?
- Serializable (OK): strings, numbers, booleans, arrays, plain objects,
Date,Map,Set - Not serializable (will error): functions, class instances, DOM nodes, React elements with closures
How do you eliminate data fetching waterfalls?
Use Promise.all to run independent fetches in parallel:
const [user, posts] = await Promise.all([
getUser(id),
getPosts(id),
]);What does the React Compiler (React Forget) change about memoization?
- The compiler automatically memoizes where safe in React 19
- You no longer need manual
useMemoanduseCallbackin most cases - Only add manual memoization if profiling reveals a specific performance issue
Gotcha: What happens if you use an array index as a key in a dynamic list?
- React cannot distinguish items correctly when items are added, removed, or reordered
- This leads to incorrect re-renders, stale state, and subtle bugs
- Always use a stable, unique identifier (e.g., database ID) as the key
What is Partial Prerendering (PPR) and when should you use it?
- PPR statically renders the page shell while streaming dynamic parts via Suspense
- Available in Next.js 15+
- Best for pages with both static and dynamic content (e.g., a dashboard with a static header and live data)
How do you choose between SSG, ISR, SSR, and CSR?
- SSG: public, rarely changing content (marketing pages)
- ISR: content that changes periodically (blog posts, product pages)
- SSR: user-specific or real-time data
- CSR: auth-gated internal tools where SEO is not needed
How do you type a Server Component that receives params in Next.js?
type Props = {
params: Promise<{ id: string }>;
};
export default async function Page({ params }: Props) {
const { id } = await params;
// ...
}How should you type the return value of useActionState in TypeScript?
Define an explicit state type for the action's return value:
type FormState = {
error?: string;
data?: { id: string };
};
const [state, action, isPending] = useActionState<FormState, FormData>(
submitAction,
{ error: undefined, data: undefined }
);Gotcha: Why should you never expose NEXT_PUBLIC_ prefixed variables for secrets?
- Any environment variable prefixed with
NEXT_PUBLIC_is inlined into the client bundle at build time - It is visible to anyone inspecting the page source or network requests
- API keys, database URLs, and tokens must use server-only env vars (no prefix)
What are the four caching layers in Next.js and what does each cache?
- Request Memoization: deduplicates identical fetches within one render pass
- Data Cache: caches fetch responses until revalidated
- Full Route Cache: caches entire rendered pages until revalidated
- Router Cache: client-side route cache that lasts for the session
How do you validate server-side inputs with Zod in a Server Action?
"use server";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
});
export async function createUser(formData: FormData) {
const result = schema.safeParse(Object.fromEntries(formData));
if (!result.success) return { error: result.error.flatten() };
}What is the purpose of useOptimistic and when should you use it?
useOptimisticprovides instant UI feedback while an async action (e.g., Server Action) is in progress- If the action fails, React automatically rolls back to the previous state
- Use it for likes, saves, deletes, and any mutation where perceived speed matters
Quick Reference Card
| Category | Key Takeaway |
|---|---|
| Architecture | Server Components first, Client Components for interactivity only |
| Performance | Parallel fetching, streaming, PPR, Turbopack |
| Data | Server Actions for mutations, Suspense for loading |
| State | Local state by default, Zustand for shared, URL for shareable |
| Forms | useActionState + Zod validation + useOptimistic |
| Security | Never expose secrets, validate on server, auth on every action |
| Testing | Unit + component + E2E covering critical paths |
| SEO | Metadata API, server rendering, structured data |