CodeWithMMAK
Next.jsAdvanced

Advanced Next.js Patterns

Learn advanced patterns for building scalable Next.js applications.

CodeWithMMAK
March 22, 2026
10 min

Advanced Patterns

In this second part of our series, we dive into advanced patterns.

Compound Components

Compound components are a powerful pattern for building flexible APIs.

Code Snippet
<Tabs>
  <Tabs.List>
    <Tabs.Trigger value="one">One</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="one">Content One</Tabs.Content>
</Tabs>

Stay tuned for more!

šŸŽÆ Quick Answer

Advanced Next.js Patterns such as Compound Components, Render Props, and the Context + Composition pattern allow you to build truly reusable, scalable component libraries that are easy to maintain and test. These patterns are the backbone of enterprise-grade Next.js applications.

Why Advanced Patterns Matter

When Next.js applications grow, naive component structures quickly become unmaintainable. Props drilling, over-reliance on global state, and tangled component hierarchies are all symptoms of skipping architectural patterns. Advanced patterns solve these real-world problems with proven, composable solutions.

Compound Components — Deep Dive

The Compound Component pattern lets a parent component share implicit state with its children via React Context, giving consumers a declarative, expressive API without prop drilling.

Code Snippet
import { createContext, useContext, useState } from 'react';

const TabsContext = createContext<{ active: string; setActive: (v: string) => void } | null>(null);

function Tabs({ defaultValue, children }: { defaultValue: string; children: React.ReactNode }) {
  const [active, setActive] = useState(defaultValue);
  return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}

Tabs.List = function TabsList({ children }: { children: React.ReactNode }) {
  return <div role="tablist" className="flex gap-2">{children}</div>;
};

Tabs.Trigger = function TabsTrigger({ value, children }: { value: string; children: React.ReactNode }) {
  const ctx = useContext(TabsContext)!;
  return (
    <button
      role="tab"
      aria-selected={ctx.active === value}
      onClick={() => ctx.setActive(value)}
      className={ctx.active === value ? 'font-bold border-b-2 border-orange-600' : 'text-zinc-500'}
    >
      {children}
    </button>
  );
};

Tabs.Content = function TabsContent({ value, children }: { value: string; children: React.ReactNode }) {
  const ctx = useContext(TabsContext)!;
  return ctx.active === value ? <div role="tabpanel">{children}</div> : null;
};

Usage:

Code Snippet
<Tabs defaultValue="one">
  <Tabs.List>
    <Tabs.Trigger value="one">Overview</Tabs.Trigger>
    <Tabs.Trigger value="two">Details</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="one">Overview content here.</Tabs.Content>
  <Tabs.Content value="two">Detailed content here.</Tabs.Content>
</Tabs>

The Render Props Pattern

Render props are useful when you need to share stateful logic without dictating the UI. While React Hooks have largely replaced this pattern for new code, it remains valuable when working with class-based components or third-party libraries.

Code Snippet
function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => React.ReactNode }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })} style={{ height: '100vh' }}>
      {render(pos)}
    </div>
  );
}

// Usage
<MouseTracker render={({ x, y }) => <p>Mouse at {x}, {y}</p>} />

Higher-Order Components (HOC)

HOCs wrap a component to inject props or behavior. They are still common in authentication guards and analytics wrappers.

Code Snippet
function withAuth<T extends object>(Component: React.ComponentType<T>) {
  return function AuthGuard(props: T) {
    const { isAuthenticated } = useAuth();
    if (!isAuthenticated) return <Redirect to="/login" />;
    return <Component {...props} />;
  };
}

const ProtectedDashboard = withAuth(Dashboard);

Server Component Composition in Next.js 15

In Next.js 15, Server Components allow you to compose data-fetching logic at the component level without waterfalls:

Code Snippet
// app/dashboard/page.tsx — Server Component
import { Suspense } from 'react';
import { UserStats } from './UserStats';
import { RecentActivity } from './RecentActivity';

export default function Dashboard() {
  return (
    <div className="grid grid-cols-2 gap-8">
      <Suspense fallback={<StatsSkeleton />}>
        <UserStats /> {/* fetches its own data */}
      </Suspense>
      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity /> {/* fetches its own data, in parallel */}
      </Suspense>
    </div>
  );
}

Each child fetches data in parallel, and each has its own Suspense boundary — delivering content to the browser as it becomes ready.

URL State Pattern

Using URL search params as state is a powerful Next.js-specific pattern. It makes state shareable, bookmarkable, and SSR-compatible:

Code Snippet
'use client';
import { useRouter, useSearchParams } from 'next/navigation';

export function FilterBar() {
  const router = useRouter();
  const params = useSearchParams();

  const setFilter = (key: string, value: string) => {
    const next = new URLSearchParams(params.toString());
    next.set(key, value);
    router.push(`?${next.toString()}`);
  };

  return (
    <select onChange={e => setFilter('category', e.target.value)} defaultValue={params.get('category') ?? ''}>
      <option value="">All</option>
      <option value="testing">Testing</option>
      <option value="devops">DevOps</option>
    </select>
  );
}

šŸš€ Step-by-Step Implementation

1

Choose the right pattern

Match the pattern to the problem: Compound Components for UI APIs, Render Props for shared logic, HOCs for cross-cutting concerns, Server Component Composition for data fetching.

2

Define the public API first

Write the consumer code (how you want to USE the component) before implementing it. This keeps APIs intuitive.

3

Use TypeScript strictly

Type your compound component sub-components, context values, and HOC generics. TypeScript catches misuse at compile time.

4

Write tests alongside

Each pattern should have unit tests for state transitions and integration tests for user interactions.

āœ… Best Practices

  • āœ”
    Prefer Hooks over Render Props for new code — they are more readable and composable.
  • āœ”
    Use the displayName property on HOCs to preserve component names in React DevTools.
  • āœ”
    Keep Compound Component context private — don't export the context, only the parent and its sub-components.
  • āœ”
    Collocate data fetching with the component that needs it using Server Components — avoid prop drilling data down from page level.

Frequently Asked Questions

When should I use Compound Components vs simple props?

Use Compound Components when a component has multiple related sub-parts that consumers need to arrange or configure independently. Simple props work fine for small, self-contained components.

Are HOCs still relevant in 2026?

Less so for new code, but they remain relevant in large codebases that haven't migrated to Hooks, and for wrapping third-party class components.

How do I test Compound Components?

Test them as a unit using React Testing Library. Render the parent with all sub-components and assert on the resulting DOM — don't test sub-components in isolation.

Conclusion

Mastering these advanced Next.js patterns gives your applications a solid architectural foundation. Compound Components keep your component APIs clean and flexible, HOCs handle cross-cutting concerns, and Server Component Composition enables efficient, parallel data fetching. Apply these patterns intentionally — the goal is maintainability and clarity, not pattern for pattern's sake.

šŸ“ Summary & Key Takeaways

Advanced Next.js patterns — Compound Components, Render Props, HOCs, and Server Component Composition — solve real architectural problems in growing applications. Compound Components share state implicitly via Context, HOCs inject cross-cutting concerns, and Server Components parallelize data fetching at the component level. The URL State pattern leverages Next.js routing to make UI state shareable and SSR-compatible. Choose patterns based on the problem, always define the consumer API first, and back every pattern with tests.

Optimistic UI Updates

Optimistic updates improve perceived performance by immediately reflecting the expected result of a user action in the UI, then reconciling with the server response when it arrives:

Code Snippet
'use client';
import { useOptimistic, useTransition } from 'react';

type Todo = { id: number; text: string; completed: boolean };

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (state, newTodo: Todo) => [...state, newTodo]
  );
  const [, startTransition] = useTransition();

  const addTodo = async (text: string) => {
    const tempTodo = { id: Date.now(), text, completed: false };
    startTransition(() => {
      addOptimistic(tempTodo);
    });
    await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }) });
  };

  return (
    <ul>
      {optimisticTodos.map(todo => (
        <li key={todo.id} className={todo.id > 1e12 ? 'opacity-50' : ''}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Items added optimistically appear immediately at full opacity but are rendered at 50% opacity while the network request is in flight.

Parallel Route Interception

Next.js 15 supports intercepting routes to show content in a modal while keeping the underlying page visible — ideal for photo galleries, login forms, and quick-view panels:

Code Snippet
app/
ā”œā”€ā”€ @modal/
│   └── (.)photo/[id]/
│       └── page.tsx     ← Intercepts /photo/[id] when navigated client-side
ā”œā”€ā”€ photo/
│   └── [id]/
│       └── page.tsx     ← Full page when navigated directly or refreshed
└── layout.tsx

This pattern delivers a modal experience on client-side navigation and a full-page experience on direct URL access or refresh — perfectly supporting social media sharing and deep linking.

Testing Advanced Patterns

Every pattern discussed in this series should be covered by tests. Here is a concise testing strategy:

Code Snippet
// Testing Compound Components
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Tabs from './Tabs';

describe('Tabs compound component', () => {
  it('shows only the active tab content', async () => {
    render(
      <Tabs defaultValue="tab1">
        <Tabs.List>
          <Tabs.Trigger value="tab1">First</Tabs.Trigger>
          <Tabs.Trigger value="tab2">Second</Tabs.Trigger>
        </Tabs.List>
        <Tabs.Content value="tab1">Content 1</Tabs.Content>
        <Tabs.Content value="tab2">Content 2</Tabs.Content>
      </Tabs>
    );

    expect(screen.getByText('Content 1')).toBeVisible();
    expect(screen.queryByText('Content 2')).not.toBeInTheDocument();

    await userEvent.click(screen.getByRole('tab', { name: 'Second' }));
    expect(screen.getByText('Content 2')).toBeVisible();
  });
});

Testing compound components as a unit — rather than testing each sub-component in isolation — gives you higher confidence that the API works correctly from a consumer's perspective.

Share it with your network and help others learn too!

Follow me on social media for more developer tips, tricks, and tutorials. Let's connect and build something great together!