React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

reactnextjsrulesbest-practicescheatsheet

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.

LayerHandlesExamples
Server ComponentsData fetching, static content, SEOProduct pages, dashboards, articles
Client ComponentsInteractivity, state, browser APIsForms, modals, dropdowns, animations

8. Use React 19's new hooks wisely. Leverage the new APIs for common patterns:

HookPurpose
useActionStateForm action state with pending, error, data
useOptimisticOptimistic UI updates during async actions
useFormStatusAccess 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 metrics

Performance & 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.

StrategyBest ForTrade-off
Static (SSG)Public content, marketing pagesStale until rebuild
ISRBlog posts, product pagesStale within revalidation window
SSRUser-specific, real-time dataSlower TTFB, more server load
CSRInternal tools, auth-gated dashboardsNo 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 LayerWhatDuration
Request MemoizationDedupes identical fetches in one renderSingle request
Data CacheCaches fetch responsesUntil revalidated
Full Route CacheCaches entire rendered pagesUntil revalidated
Router CacheClient-side route cacheSession-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.

MetricTargetHow
LCP (Largest Contentful Paint)Under 2.5sPriority images, preload fonts, SSR
CLS (Cumulative Layout Shift)Under 0.1Set image dimensions, avoid layout shifts
INP (Interaction to Next Paint)Under 200msSmall 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 --turbopack

23. 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 TypeSolution
UI state (one component)useState
Complex local stateuseReducer
Shared across siblingsLift state to parent
Shared across appZustand or Context
Server dataServer Components, SWR, TanStack Query
URL stateuseSearchParams

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 client

37. 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 TypeToolWhat to Test
UnitVitestHooks, utilities, Server Actions
ComponentReact Testing LibraryUser interactions, rendering
E2EPlaywrightCritical 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 alt text
  • 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 useMemo and useCallback in 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?
  • useOptimistic provides 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

CategoryKey Takeaway
ArchitectureServer Components first, Client Components for interactivity only
PerformanceParallel fetching, streaming, PPR, Turbopack
DataServer Actions for mutations, Suspense for loading
StateLocal state by default, Zustand for shared, URL for shareable
FormsuseActionState + Zod validation + useOptimistic
SecurityNever expose secrets, validate on server, auth on every action
TestingUnit + component + E2E covering critical paths
SEOMetadata API, server rendering, structured data