Skip to content

1Kyryll/React-NextJS-Senior-Guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

Senior Engineer Study Guide: React & Next.js (Advanced)

Everything a senior-level React/Next.js engineer should deeply understand — from rendering strategies to production system design.


Table of Contents

  1. React Performance & Memoization
  2. React Concurrent Features
  3. Next.js Rendering Strategies (SSR, SSG, ISR)
  4. Server Components & Server Actions
  5. Optimistic UI
  6. Data Fetching Patterns
  7. State Management at Scale
  8. Caching Architecture
  9. Production System Design
  10. Advanced Patterns & Architecture
  11. Testing Strategy
  12. Recommended Resources

1. React Performance & Memoization

1.1 useMemo

useMemo caches the result of an expensive computation between re-renders.

const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.price - b.price);
}, [items]);

When to use:

  • Filtering/sorting large lists (thousands of items).
  • Deriving complex objects passed as props to memoized children.
  • Expensive mathematical or formatting computations.

When NOT to use:

  • Simple arithmetic, string concatenation, or trivial object literals — the overhead of memoization itself exceeds the cost.
  • As a "default" on every variable. Profile first.

Mental model: useMemo is a cache with a dependency array as key. React may discard the cache at any time (e.g., offscreen components). Never rely on it for correctness — only for performance.

Common mistake — unstable dependencies:

// BAD: `filter` is a new object every render → useMemo re-runs every time
function ProductList({ filter }) {
  const results = useMemo(() => expensiveFilter(data, filter), [filter]);
}
 
// FIX: memoize `filter` at the parent, or destructure primitives
function ProductList({ category, minPrice }) {
  const results = useMemo(
    () => expensiveFilter(data, category, minPrice),
    [category, minPrice]
  );
}

1.2 useCallback

useCallback caches a function definition between renders. It is syntactic sugar over useMemo(() => fn, deps).

const handleDelete = useCallback((id: string) => {
  setItems(prev => prev.filter(item => item.id !== id));
}, []); // stable reference — safe to pass to React.memo children

Primary purpose: Prevent re-renders of memoized child components that receive callbacks as props.

// Without useCallback, <ExpensiveList> re-renders on every parent render
// because `onDelete` is a new function reference each time.
const Parent = () => {
  const onDelete = useCallback((id) => { /* ... */ }, []);
  return <ExpensiveList onDelete={onDelete} />;
};
 
const ExpensiveList = React.memo(({ onDelete }) => {
  // renders 10,000 rows
});

The useCallback + React.memo contract: useCallback alone does nothing useful. It only helps when the consumer of the callback is wrapped in React.memo (or uses the callback in a dependency array of its own hooks). If there is no memoized consumer, you are paying the cost of memoization for zero benefit.

1.3 React.memo

Higher-order component that skips re-rendering when props are shallowly equal.

const Row = React.memo(function Row({ item, onSelect }: Props) {
  return <div onClick={() => onSelect(item.id)}>{item.name}</div>;
});

Custom comparator:

const Row = React.memo(Component, (prevProps, nextProps) => {
  // return true to SKIP re-render, false to re-render
  return prevProps.item.id === nextProps.item.id
      && prevProps.item.updatedAt === nextProps.item.updatedAt;
});

1.4 The Composition Alternative

Often the best "performance optimization" is restructuring your component tree so that state lives closer to where it's used — eliminating unnecessary re-renders without any memoization.

// BEFORE: entire page re-renders on every keystroke
function Page() {
  const [query, setQuery] = useState('');
  return (
    <div>
      <SearchBar query={query} onChange={setQuery} />
      <HeavyDashboard /> {/* re-renders needlessly */}
    </div>
  );
}
 
// AFTER: extract stateful part — HeavyDashboard never re-renders
function Page() {
  return (
    <div>
      <SearchSection /> {/* state is inside */}
      <HeavyDashboard />
    </div>
  );
}

1.5 useRef for Stable References

// Store latest callback without triggering re-renders
const callbackRef = useRef(onSave);
useEffect(() => { callbackRef.current = onSave; });
 
// Use in event listeners, intervals, etc.
useEffect(() => {
  const id = setInterval(() => callbackRef.current(), 1000);
  return () => clearInterval(id);
}, []); // no dependency on onSave — interval never resets

1.6 Profiling Workflow

  1. React DevTools Profiler — record renders, find "why did this render?"
  2. Chrome Performance tab — measure actual JS execution time.
  3. React.Profiler component — programmatic render timing in production.
  4. Rule: measure → identify bottleneck → apply targeted fix. Never scatter useMemo/useCallback everywhere preemptively.

2. React Concurrent Features

2.1 useTransition

Marks a state update as non-urgent. React can interrupt rendering to keep the UI responsive.

const [isPending, startTransition] = useTransition();
 
function handleSearch(query: string) {
  // Urgent: update input immediately
  setInputValue(query);
 
  // Non-urgent: can be interrupted
  startTransition(() => {
    setSearchResults(filterLargeDataset(query));
  });
}

Production use case: Filtering a 50,000-row table. The input stays snappy while the table update is deferred.

2.2 useDeferredValue

Creates a "lagging" copy of a value. Equivalent to wrapping the consumer in startTransition.

const deferredQuery = useDeferredValue(query);
// deferredQuery may lag behind `query` during heavy renders
 
const filteredItems = useMemo(
  () => items.filter(i => i.name.includes(deferredQuery)),
  [deferredQuery, items]
);

2.3 <Suspense> for Data and Code

<Suspense fallback={<Skeleton />}>
  <LazyComponent />      {/* code splitting */}
  <DataComponent />       {/* data fetching with a Suspense-enabled library */}
</Suspense>

Nested Suspense boundaries let you control granularity: show a full-page skeleton or individual component skeletons.

2.4 React use() Hook (React 19+)

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise); // suspends until resolved
  return comments.map(c => <Comment key={c.id} {...c} />);
}

use() can also read Context — it replaces useContext and works inside conditionals and loops.


3. Next.js Rendering Strategies

3.1 Comparison Table

Strategy Build time Request time Revalidation Use case
SSG HTML generated Served from CDN Manual redeploy Marketing pages, docs
ISR Initial HTML generated Served stale, revalidated in background Time-based or on-demand Blog posts, product pages
SSR HTML generated per request Every request Personalized dashboards, auth-gated
CSR JS renders in browser Client fetches SPAs behind auth, real-time UIs
Streaming SSR HTML streamed progressively Every request Complex pages with mixed priorities
PPR (Partial Prerendering) Static shell generated Dynamic holes streamed Hybrid E-commerce (static layout + dynamic cart)

3.2 SSG (Static Site Generation)

In the App Router, any page that doesn't use dynamic functions (cookies(), headers(), searchParams) is statically generated by default.

// app/about/page.tsx — automatically SSG
export default function AboutPage() {
  return <div>About us</div>;
}
 
// Force static even with a fetch
export const dynamic = 'force-static';
 
// Generate static params for dynamic routes
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({ slug: post.slug }));
}

3.3 ISR (Incremental Static Regeneration)

// Time-based: revalidate every 60 seconds
export const revalidate = 60;
 
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return <ProductView product={product} />;
}

On-demand revalidation (webhook-driven):

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
 
export async function POST(request: Request) {
  const { tag, secret } = await request.json();
 
  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }
 
  revalidateTag(tag);          // invalidate all fetches tagged with this
  // OR revalidatePath('/products'); // invalidate a specific path
 
  return Response.json({ revalidated: true, now: Date.now() });
}

ISR architecture in production:

CMS webhook → /api/revalidate → revalidateTag('products')
                                      ↓
              Next server marks cached pages stale
                                      ↓
              Next request triggers background regeneration
                                      ↓
              New HTML served on subsequent requests

3.4 SSR (Server-Side Rendering)

Force SSR by using dynamic functions or the route config:

export const dynamic = 'force-dynamic';
 
// Or simply use cookies/headers — this opts into SSR automatically
import { cookies } from 'next/headers';
 
export default async function DashboardPage() {
  const session = (await cookies()).get('session');
  const data = await getDashboardData(session.value);
  return <Dashboard data={data} />;
}

3.5 Streaming SSR

Stream HTML progressively — show important content first while slower parts load:

import { Suspense } from 'react';
 
export default function ProductPage({ params }) {
  return (
    <div>
      {/* Sent immediately */}
      <ProductHeader id={params.id} />
 
      {/* Streamed when ready */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews id={params.id} />
      </Suspense>
 
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations id={params.id} />
      </Suspense>
    </div>
  );
}

3.6 Partial Prerendering (PPR)

Next.js experimental feature: static shell + dynamic holes.

// next.config.js
module.exports = { experimental: { ppr: true } };
 
// app/page.tsx
export default function StorePage() {
  return (
    <div>
      <StaticNavbar />         {/* pre-rendered at build time */}
      <StaticHeroBanner />
 
      <Suspense fallback={<CartSkeleton />}>
        <DynamicCartWidget />   {/* streamed at request time */}
      </Suspense>
    </div>
  );
}

Why this matters: You get CDN-speed for the shell + personalization for specific components. Best of both worlds.


4. Server Components & Server Actions

4.1 React Server Components (RSC)

Server Components run only on the server. They can directly access databases, file systems, and secrets. They send rendered HTML (not JS) to the client — zero bundle size impact.

// app/posts/page.tsx — Server Component by default
import { db } from '@/lib/db';
 
export default async function PostsPage() {
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  });
 
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}
// No useState, no useEffect, no "loading state" — data is already here.

Key rules:

  • Server Components cannot use hooks (useState, useEffect, etc.).
  • Server Components cannot use browser APIs (window, document).
  • Client Components cannot import Server Components directly — pass them as children or other props.

The boundary pattern:

// Server Component
import { ClientInteractiveWidget } from './ClientWidget';
 
export default async function Page() {
  const data = await fetchData(); // server-only
  return (
    <ClientInteractiveWidget initialData={data}>
      <ServerRenderedContent />  {/* passed as children — stays on server */}
    </ClientInteractiveWidget>
  );
}

4.2 Server Actions

Functions that run on the server but are callable from the client. They replace API routes for mutations.

// app/actions/posts.ts
'use server';
 
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { z } from 'zod';
 
const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1),
});
 
export async function createPost(formData: FormData) {
  const parsed = CreatePostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });
 
  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors };
  }
 
  await db.post.create({ data: parsed.data });
  revalidatePath('/posts');
  return { success: true };
}

Using in a Client Component:

'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions/posts';
 
export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null);
 
  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="content" required />
      {state?.error && <p className="text-red-500">{JSON.stringify(state.error)}</p>}
      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

Security considerations:

  • Always validate inputs. Server Actions are publicly accessible endpoints.
  • Always check authorization — cookies(), session tokens.
  • Use z.object() (Zod) or similar for runtime validation.
  • Rate-limit Server Actions in production (middleware or external).

5. Optimistic UI

5.1 useOptimistic (React 19+)

Show the expected result immediately before the server confirms.

'use client';
import { useOptimistic } from 'react';
import { toggleLike } from '@/app/actions/likes';
 
export function LikeButton({ postId, initialLiked, initialCount }) {
  const [optimistic, setOptimistic] = useOptimistic(
    { liked: initialLiked, count: initialCount },
    (current, newLiked: boolean) => ({
      liked: newLiked,
      count: current.count + (newLiked ? 1 : -1),
    })
  );
 
  async function handleClick() {
    const newLiked = !optimistic.liked;
    setOptimistic(newLiked);       // instant UI update
    await toggleLike(postId);      // server mutation
    // On success: server-revalidated data replaces optimistic state
    // On failure: React automatically rolls back
  }
 
  return (
    <button onClick={handleClick}>
      {optimistic.liked ? '❤️' : '🤍'} {optimistic.count}
    </button>
  );
}

5.2 Optimistic List Operations

'use client';
import { useOptimistic } from 'react';
 
type Todo = { id: string; text: string; completed: boolean };
 
export function TodoList({ todos, addTodoAction }) {
  const [optimisticTodos, addOptimistic] = useOptimistic<Todo[], string>(
    todos,
    (state, newText) => [
      ...state,
      { id: `temp-${Date.now()}`, text: newText, completed: false },
    ]
  );
 
  async function handleSubmit(formData: FormData) {
    const text = formData.get('text') as string;
    addOptimistic(text);            // show immediately with temp ID
    await addTodoAction(formData);  // server replaces temp with real ID
  }
 
  return (
    <>
      <form action={handleSubmit}>
        <input name="text" />
        <button>Add</button>
      </form>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{
            opacity: todo.id.startsWith('temp-') ? 0.6 : 1
          }}>
            {todo.text}
          </li>
        ))}
      </ul>
    </>
  );
}

5.3 When Optimistic UI Makes Sense

Good candidates: Likes/votes, adding comments, toggling settings, reordering lists, marking items as read.

Bad candidates: Payment processing, inventory reservations (stock could be zero), anything with complex server-side validation you cannot predict client-side.


6. Data Fetching Patterns

6.1 Parallel vs. Sequential Fetching

// SEQUENTIAL — waterfall (slow)
export default async function Page() {
  const user = await getUser();          // 200ms
  const posts = await getPosts(user.id); // 300ms — waits for user
  // Total: ~500ms
}
 
// PARALLEL — concurrent (fast)
export default async function Page() {
  const userPromise = getUser();
  const postsPromise = getPosts();
  const [user, posts] = await Promise.all([userPromise, postsPromise]);
  // Total: ~300ms (max of both)
}

6.2 Fetch Deduplication & Caching

Next.js automatically deduplicates fetch() calls with the same URL and options within a single render pass.

// These two fetches in different components result in ONE network request:
// ComponentA.tsx
const data = await fetch('https://api.example.com/data');
 
// ComponentB.tsx
const data = await fetch('https://api.example.com/data');

Cache control:

// Cache indefinitely (default for SSG)
fetch(url, { cache: 'force-cache' });
 
// Never cache (SSR behavior)
fetch(url, { cache: 'no-store' });
 
// Revalidate after N seconds (ISR behavior)
fetch(url, { next: { revalidate: 3600 } });
 
// Tag-based invalidation
fetch(url, { next: { tags: ['products'] } });

6.3 React Query / TanStack Query (Client-Side)

For client-side data fetching where you need caching, refetching, pagination, and optimistic updates:

'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 
function Comments({ postId }) {
  const queryClient = useQueryClient();
 
  const { data, isLoading } = useQuery({
    queryKey: ['comments', postId],
    queryFn: () => fetchComments(postId),
    staleTime: 5 * 60 * 1000,     // 5 min before refetch
    gcTime: 30 * 60 * 1000,       // 30 min cache lifetime
  });
 
  const mutation = useMutation({
    mutationFn: addComment,
    onMutate: async (newComment) => {
      // Cancel in-flight queries
      await queryClient.cancelQueries({ queryKey: ['comments', postId] });
 
      // Snapshot
      const previous = queryClient.getQueryData(['comments', postId]);
 
      // Optimistic update
      queryClient.setQueryData(['comments', postId], (old) => [
        ...old,
        { ...newComment, id: `temp-${Date.now()}` },
      ]);
 
      return { previous };
    },
    onError: (err, vars, context) => {
      // Rollback
      queryClient.setQueryData(['comments', postId], context.previous);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['comments', postId] });
    },
  });
}

7. State Management at Scale

7.1 Decision Framework

Scenario Recommendation
Form state, toggles, local UI useState / useReducer
Shared UI state (modals, themes) React Context + useReducer
Server-derived state RSC + Server Actions (or React Query)
Global client state (rare) Zustand, Jotai
Complex cross-cutting state Zustand with slices
URL-driven state nuqs or useSearchParams

7.2 Zustand — Minimal Global Store

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
 
interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  total: () => number;
}
 
export const useCartStore = create<CartStore>()(
  devtools(
    persist(
      (set, get) => ({
        items: [],
        addItem: (item) =>
          set((state) => ({ items: [...state.items, item] }), false, 'addItem'),
        removeItem: (id) =>
          set((state) => ({
            items: state.items.filter((i) => i.id !== id),
          }), false, 'removeItem'),
        total: () => get().items.reduce((sum, i) => sum + i.price * i.qty, 0),
      }),
      { name: 'cart-storage' }
    )
  )
);

7.3 URL State with nuqs

import { useQueryState, parseAsInteger } from 'nuqs';
 
export function ProductFilters() {
  const [category, setCategory] = useQueryState('category');
  const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
  const [sort, setSort] = useQueryState('sort', { defaultValue: 'popular' });
  // URL: /products?category=shoes&page=2&sort=price
}

8. Caching Architecture

8.1 Next.js Four-Layer Cache

Request → (1) Router Cache (client, in-memory)
        → (2) Full Route Cache (server, filesystem)
        → (3) Data Cache (server, fetch results)
        → (4) React Cache / request memoization (per-request dedup)
Layer Location Duration Invalidation
Router Cache Client Session / 30s–5min auto router.refresh(), navigation, revalidatePath()
Full Route Cache Server Until revalidation revalidatePath(), revalidateTag(), redeploy
Data Cache Server Until revalidation revalidateTag(), time-based, cache: 'no-store'
Request Memoization Server Single request Automatic

8.2 unstable_cache / "use cache" (Canary)

Cache expensive computations at the data layer:

import { unstable_cache } from 'next/cache';
 
const getCachedProducts = unstable_cache(
  async (category: string) => {
    return db.product.findMany({ where: { category } });
  },
  ['products'],               // cache key prefix
  { revalidate: 300, tags: ['products'] }
);

8.3 Production Caching Strategy

                    ┌─────────────────────────┐
                    │      CDN (Vercel/CF)     │  TTL: varies by route
                    └────────────┬────────────┘
                                 │
                    ┌────────────▼────────────┐
                    │   Next.js Server Cache   │  Full Route + Data Cache
                    └────────────┬────────────┘
                                 │
              ┌──────────────────┼──────────────────┐
              ▼                  ▼                   ▼
     ┌────────────┐    ┌────────────────┐   ┌──────────────┐
     │  Redis /    │    │   Database     │   │  External    │
     │  Memcached  │    │   (Postgres)   │   │  APIs        │
     └────────────┘    └────────────────┘   └──────────────┘

Rules of thumb:

  • Static pages → CDN with long TTL + on-demand revalidation.
  • Personalized pages → no CDN cache, use Vary headers or skip cache entirely.
  • Shared expensive queries → Redis/Memcached with tag-based invalidation.
  • User-specific queries → short-lived or no cache.

9. Production System Design

9.1 Error Handling Architecture

// app/error.tsx — catches errors in the nearest segment
'use client';
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log to monitoring (Sentry, Datadog, etc.)
    captureException(error);
  }, [error]);
 
  return (
    <div>
      <h2>Something went wrong</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
 
// app/global-error.tsx — catches root layout errors
// app/not-found.tsx — custom 404

Error boundary granularity: Place error.tsx at each route segment level. A failing <Reviews> section shouldn't crash the entire product page.

9.2 Middleware

Runs before every request at the edge.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
 
export function middleware(request: NextRequest) {
  // Auth check
  const session = request.cookies.get('session');
  if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
 
  // A/B testing
  const bucket = request.cookies.get('ab-bucket');
  if (!bucket) {
    const response = NextResponse.next();
    response.cookies.set('ab-bucket', Math.random() > 0.5 ? 'A' : 'B');
    return response;
  }
 
  // Geolocation-based routing
  const country = request.geo?.country || 'US';
  if (country === 'DE' && !request.nextUrl.pathname.startsWith('/de')) {
    return NextResponse.redirect(new URL('/de' + request.nextUrl.pathname, request.url));
  }
 
  return NextResponse.next();
}
 
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

9.3 Performance Budgets & Monitoring

Core Web Vitals targets:

  • LCP (Largest Contentful Paint) < 2.5s
  • INP (Interaction to Next Paint) < 200ms
  • CLS (Cumulative Layout Shift) < 0.1

Monitoring stack:

  • Real User Monitoring (RUM): Vercel Analytics, Datadog RUM, or custom web-vitals reporting.
  • Synthetic monitoring: Lighthouse CI in your pipeline.
  • Error tracking: Sentry with source maps.
  • Logging: structured JSON logs → aggregator (Datadog, Grafana).

9.4 Image & Font Optimization

import Image from 'next/image';
 
// Automatic WebP/AVIF, lazy loading, srcset generation
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority           // above the fold — disable lazy loading
  sizes="(max-width: 768px) 100vw, 50vw"
  placeholder="blur"
  blurDataURL={base64BlurHash}
/>
 
// Font optimization — no layout shift
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });

9.5 Security Hardening

// next.config.js
const securityHeaders = [
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=()' },
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'"
  },
];
 
module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }];
  },
};

9.6 Bundle Analysis & Code Splitting

# Install
npm install @next/bundle-analyzer
 
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({ /* config */ });
 
# Run
ANALYZE=true npm run build

Dynamic imports for heavy libraries:

import dynamic from 'next/dynamic';
 
const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // only load client-side
});
 
// Or manual lazy loading
const { PDFViewer } = await import('react-pdf');

10. Advanced Patterns & Architecture

10.1 Feature-Based Project Structure

src/
├── app/                     # Next.js routing
│   ├── (auth)/              # Route group — shared layout, no URL segment
│   │   ├── login/
│   │   └── register/
│   ├── (dashboard)/
│   │   ├── layout.tsx       # Dashboard shell
│   │   ├── overview/
│   │   └── settings/
│   └── api/
├── features/                # Domain-driven modules
│   ├── auth/
│   │   ├── components/
│   │   ├── actions/
│   │   ├── hooks/
│   │   ├── lib/
│   │   └── types.ts
│   ├── products/
│   └── cart/
├── shared/                  # Cross-cutting concerns
│   ├── components/          # Design system primitives
│   ├── hooks/
│   ├── lib/                 # Utilities, API clients
│   └── types/
└── infrastructure/          # DB, auth, monitoring config
    ├── db/
    ├── auth/
    └── monitoring/

10.2 Parallel & Intercepting Routes

app/
├── @modal/                  # Parallel route — renders alongside main content
│   └── (.)photo/[id]/       # Intercepting route — catches /photo/[id] navigations
│       └── page.tsx         # Shows as a modal on client navigation
├── photo/[id]/
│   └── page.tsx             # Full page on direct URL access or refresh
└── layout.tsx               # Renders {children} + {modal}
// app/layout.tsx
export default function Layout({ children, modal }) {
  return (
    <>
      {children}
      {modal}
    </>
  );
}

10.3 Compound Component Pattern

// Headless compound component — consumer controls rendering
const Tabs = ({ children, defaultValue }) => {
  const [active, setActive] = useState(defaultValue);
  return (
    <TabsContext.Provider value={{ active, setActive }}>
      {children}
    </TabsContext.Provider>
  );
};
 
Tabs.List = ({ children }) => <div role="tablist">{children}</div>;
Tabs.Trigger = ({ value, children }) => {
  const { active, setActive } = useTabsContext();
  return (
    <button role="tab" aria-selected={active === value} onClick={() => setActive(value)}>
      {children}
    </button>
  );
};
Tabs.Content = ({ value, children }) => {
  const { active } = useTabsContext();
  return active === value ? <div role="tabpanel">{children}</div> : null;
};
 
// Usage
<Tabs defaultValue="overview">
  <Tabs.List>
    <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
    <Tabs.Trigger value="analytics">Analytics</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="overview"><Overview /></Tabs.Content>
  <Tabs.Content value="analytics"><Analytics /></Tabs.Content>
</Tabs>

10.4 Render Props & Hooks Composition

// Custom hook encapsulating complex logic
function useInfiniteScroll<T>({
  fetchFn,
  pageSize = 20,
}: {
  fetchFn: (cursor: string | null) => Promise<{ items: T[]; nextCursor: string | null }>;
  pageSize?: number;
}) {
  const [items, setItems] = useState<T[]>([]);
  const [cursor, setCursor] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const observerRef = useRef<IntersectionObserver>();
  const sentinelRef = useCallback((node: HTMLElement | null) => {
    if (observerRef.current) observerRef.current.disconnect();
    if (!node || !hasMore) return;
    observerRef.current = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) loadMore();
    });
    observerRef.current.observe(node);
  }, [hasMore, cursor]);
 
  async function loadMore() {
    if (isLoading) return;
    setIsLoading(true);
    const { items: newItems, nextCursor } = await fetchFn(cursor);
    setItems(prev => [...prev, ...newItems]);
    setCursor(nextCursor);
    setHasMore(!!nextCursor);
    setIsLoading(false);
  }
 
  return { items, isLoading, hasMore, sentinelRef };
}

11. Testing Strategy

11.1 Testing Pyramid for Next.js

           ┌───────────┐
           │   E2E     │  Playwright — critical user flows
           │ (few)     │  Login → checkout → confirmation
           ├───────────┤
           │Integration│  Testing Library — page-level
           │ (some)    │  Component + hooks + API mocks
           ├───────────┤
           │  Unit     │  Vitest — pure logic
           │ (many)    │  Utils, hooks, reducers, validators
           └───────────┘

11.2 Testing Server Components

// __tests__/PostsPage.test.tsx
import { render, screen } from '@testing-library/react';
import PostsPage from '@/app/posts/page';
 
// Mock the data layer, not the component
vi.mock('@/lib/db', () => ({
  db: {
    post: {
      findMany: vi.fn().mockResolvedValue([
        { id: '1', title: 'Test Post', excerpt: 'Content' },
      ]),
    },
  },
}));
 
test('renders posts from database', async () => {
  const Page = await PostsPage();      // Server Components are async functions
  render(Page);
  expect(screen.getByText('Test Post')).toBeInTheDocument();
});

11.3 Testing Server Actions

import { createPost } from '@/app/actions/posts';
 
test('validates input', async () => {
  const formData = new FormData();
  formData.set('title', '');   // invalid
  formData.set('content', 'x');
 
  const result = await createPost(formData);
  expect(result.error.title).toBeDefined();
});

12. Recommended Resources

Official Documentation

  • React docs — react.dev (especially the "Escape Hatches" and "Learn" sections)
  • Next.js docs — nextjs.org/docs (App Router section, Caching deep-dive)
  • Vercel blog — engineering articles on PPR, RSC internals

Deep Dives

  • Dan Abramov's "A Complete Guide to useEffect" — overreacted.io
  • Josh Comeau's blog — joshwcomeau.com (visual explanations of React rendering, Server Components)
  • Kent C. Dodds's blog — kentcdodds.com (testing philosophy, component patterns)
  • TkDodo's blog — tkdodo.eu (authoritative TanStack Query content)
  • Lee Robinson's YouTube — Next.js patterns, App Router migrations

Architecture & System Design

  • Patterns.dev — patterns.dev (rendering patterns, performance patterns)
  • Web.dev — web.dev/learn (Core Web Vitals, performance)
  • "Bulletproof React" — github.com/alan2207/bulletproof-react (project structure reference)

Video Courses

  • Theo Browne (t3.gg) — YouTube for Next.js opinions and architecture takes
  • Jack Herrington — YouTube for advanced React patterns and performance
  • "React — The Full Course" by ui.dev — deep mental model of React internals

Books

  • Fluent React by Tejas Kumar (O'Reilly) — React internals, Fiber, reconciliation
  • Learning Patterns by Lydia Hallie & Addy Osmani — design/rendering patterns for the web

About

React and Next JS Senior/Production level study guide generated with official documentations and CLAUDE Opus 4.6 Model.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors