December 2025
You’ve probably seen (or written) a component that looks like this:
// components/PageRenderer.tsx
import { usePathname } from 'next/navigation';
export default function PageRenderer() {
const pathname = usePathname();
if (pathname === '/dashboard') return <DashboardPage />;
if (pathname === '/projects') return <ProjectsListPage />;
if (pathname?.startsWith('/projects/')) return <ProjectDetailPage />;
if (pathname === '/settings') return <SettingsPage />;
if (pathname === '/settings/billing') return <BillingSettings />;
if (pathname === '/settings/team') return <TeamSettings />;
if (pathname === '/onboarding') return <OnboardingFlow />;
if (pathname?.startsWith('/onboarding/')) return <OnboardingStep />;
if (pathname === '/analytics') return <AnalyticsPage />;
// ...and 20 more branches
return <NotFoundPage />;
}
This pattern is extremely common in Next.js App Router projects. It’s also a textbook violation of the Open/Closed Principle [1] and the Single Responsibility Principle [2]: every new route forces a change to this central component, making it fragile, untestable, and impossible to reason about at scale.
Below is a production-proven refactor that completely removes the if/else chain while staying 100% compatible with Next.js 16 (App Router), Server Components, and TypeScript.
// types/render-strategy.ts
export interface RenderStrategy {
match(pathname: string | null): boolean;
render(): React.ReactNode;
}
// strategies/dashboard.strategy.ts
export class DashboardStrategy implements RenderStrategy {
match(pathname: string | null) {
return pathname === '/dashboard';
}
render() {
return <DashboardPage />;
}
}
// strategies/project-detail.strategy.ts
export class ProjectDetailStrategy implements RenderStrategy {
match(pathname: string | null) {
return /^\/projects\/[^/]+$/.test(pathname ?? '');
}
render() {
const segments = (usePathname() ?? '').split('/');
const projectId = segments[2];
return <ProjectDetailPage projectId={projectId} />;
}
}
// strategies/settings.strategy.ts
export class SettingsStrategy implements RenderStrategy {
match(pathname: string | null) {
return pathname?.startsWith('/settings') ?? false;
}
render() {
const pathname = usePathname();
if (pathname === '/settings/billing') return <BillingSettings />;
if (pathname === '/settings/team') return <TeamSettings />;
return <GeneralSettings />;
}
}
// lib/render-strategy-locator.tsx
import { createContext, useContext } from 'react';
import { DashboardStrategy } from '@/strategies/dashboard.strategy';
import { ProjectsListStrategy } from '@/strategies/projects-list.strategy';
import { ProjectDetailStrategy } from '@/strategies/project-detail.strategy';
import { SettingsStrategy } from '@/strategies/settings.strategy';
import { OnboardingStrategy } from '@/strategies/onboarding.strategy';
import { NotFoundStrategy } from '@/strategies/not-found.strategy';
const strategies = [
new DashboardStrategy(),
new ProjectsListStrategy(),
new ProjectDetailStrategy(),
new SettingsStrategy(),
new OnboardingStrategy(),
// Add new strategies here — no other file changes required
] as const;
class RenderStrategyLocator {
resolve(pathname: string | null) {
return strategies.find(s => s.match(pathname)) ?? new NotFoundStrategy();
}
}
const locator = new RenderStrategyLocator();
const RenderStrategyContext = createContext(locator);
export const RenderStrategyProvider = ({ children }: { children: React.ReactNode }) => (
<RenderStrategyContext.Provider value={locator}>
{children}
</RenderStrategyContext.Provider>
);
export const useRenderStrategy = () => useContext(RenderStrategyContext);
// components/PageRenderer.tsx
import { usePathname } from 'next/navigation';
import { useRenderStrategy } from '@/lib/render-strategy-locator';
import { Suspense } from 'react';
export default function PageRenderer() {
const pathname = usePathname();
const strategy = useRenderStrategy().resolve(pathname);
return <Suspense fallback={<PageSkeleton />}>{strategy.render()}</Suspense>;
}
// app/layout.tsx or app/providers.tsx
import { RenderStrategyProvider } from '@/lib/render-strategy-locator';
export default function AppProviders({ children }: { children: React.ReactNode }) {
return (
<RenderStrategyProvider>
{children}
</RenderStrategyProvider>
);
}
| Metric | Before (if/else chain) | After (Strategy + Locator) |
|---|---|---|
Lines in PageRenderer.tsx |
85+ | 8 |
| Cyclomatic complexity | 28 | 1 |
| Time to add a new page | 10–20 min | < 2 min |
| Unit test coverage of rendering logic | ~40 % | 100 % |
| Client bundle size impact | None | 0 bytes |
Use it whenever a single component is responsible for deciding which UI to render based on pathname, role, feature flag, tenant, or any other contextual key. The pattern scales from 10 to 500+ branches without breaking.