Skip to main content

Stack Decisions

Framework and library selection criteria for frontend projects.

Decision Matrix

Framework Selection

Project TypeRecommendedAlternativesWhy
Marketing SiteNext.js 14+ (App Router)Astro, SvelteKitSSG, SEO, edge deploy
SaaS DashboardNext.js 14+ (App Router)Remix, SvelteKitSSR, auth, API routes
Internal ToolNext.js 14+ (App Router)Vite + ReactFast iteration, preview URLs
Blog/DocsDocusaurus, AstroNext.js MDXContent-focused, MDX native
E-commerceNext.js 14+ (App Router)Remix, HydrogenSSR for SEO, Stripe/Shopify
Mobile-first PWANext.js 14+ (App Router)SvelteKitOffline support, service workers

Styling Selection

NeedRecommendedWhen to Choose
Rapid prototypingTailwind CSSAlways for new projects
Complex themingTailwind + CSS variablesDesign system with dark mode
Animation-heavyFramer Motion + TailwindMarketing, portfolio sites
Existing design systemCSS ModulesTeam has existing system
CSS-in-JS requiredstyled-componentsLegacy codebase, specific need

Component Library Selection

NeedRecommendedWhy
Full customizationshadcn/uiCopy-paste, own the code
Accessibility-firstRadix UI primitivesUnstyled, ARIA complete
Fast prototypingshadcn/ui + react-hook-formForm patterns included
EnterpriseRadix + custom design systemFull control, accessibility
Animation-heavyshadcn/ui + Framer MotionComposable, flexible

Deep Dive: Next.js 14+ App Router

Why App Router (not Pages Router)

// App Router advantages:
const appRouterBenefits = {
serverComponents: "Less JS shipped, faster initial load",
streaming: "Incremental rendering, better UX",
layouts: "Shared UI without re-mounting",
loading: "Built-in loading states",
error: "Built-in error boundaries",
parallel: "Parallel route rendering",
intercepting: "Modal routes without navigation"
};

// When Pages Router is still valid:
const pagesRouterStillValid = {
existingApp: "Migration cost too high",
specificAPI: "getServerSideProps patterns",
team: "Team knows Pages well"
};
app/
├── (marketing)/ # Route group (no path segment)
│ ├── page.tsx # /
│ ├── about/page.tsx # /about
│ └── pricing/page.tsx # /pricing
├── (app)/ # Authenticated app
│ ├── layout.tsx # Shared auth layout
│ ├── dashboard/page.tsx # /dashboard
│ └── settings/page.tsx # /settings
├── api/ # API routes
│ └── [...route]/route.ts
├── components/ # Shared components
├── lib/ # Utilities
└── styles/ # Global styles

Data Fetching Patterns

// Server Component (default in App Router)
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // ISR: revalidate every hour
});
return <DataDisplay data={data} />;
}

// Client Component (when needed)
'use client';
export function InteractiveWidget() {
const [state, setState] = useState();
// Client-side interactivity
}

// Streaming with Suspense
export default function Page() {
return (
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
);
}

Deep Dive: Tailwind CSS

Configuration Best Practices

// tailwind.config.ts
import type { Config } from 'tailwindcss';

const config: Config = {
darkMode: 'class',
content: [
'./app/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
],
theme: {
extend: {
// Design token integration
colors: {
brand: {
50: 'var(--brand-50)',
// ...
900: 'var(--brand-900)',
},
},
// Typography scale
fontSize: {
'display-1': ['4.5rem', { lineHeight: '1.1', fontWeight: '700' }],
'display-2': ['3.75rem', { lineHeight: '1.1', fontWeight: '700' }],
},
// Animation
keyframes: {
shimmer: {
'100%': { transform: 'translateX(100%)' },
},
},
animation: {
shimmer: 'shimmer 1.5s infinite',
},
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
};

export default config;

Utility Patterns

// cn() helper (shadcn pattern)
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

// Usage
<button className={cn(
"px-4 py-2 rounded-lg font-medium",
variant === "primary" && "bg-blue-500 text-white",
variant === "secondary" && "bg-gray-100 text-gray-900",
className
)} />

Deep Dive: State Management

When to Use What

ComplexitySolutionExample
SimpleuseState/useReducerForm state, toggles
Cross-componentReact ContextTheme, auth user
Complex globalZustandShopping cart, composer state
Server stateTanStack QueryAPI data, caching
Formsreact-hook-form + zodComplex forms, validation

Zustand Pattern

// stores/composer.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface ComposerState {
selectedPatterns: string[];
addPattern: (id: string) => void;
removePattern: (id: string) => void;
clear: () => void;
}

export const useComposerStore = create<ComposerState>()(
persist(
(set) => ({
selectedPatterns: [],
addPattern: (id) => set((state) => ({
selectedPatterns: [...state.selectedPatterns, id]
})),
removePattern: (id) => set((state) => ({
selectedPatterns: state.selectedPatterns.filter(p => p !== id)
})),
clear: () => set({ selectedPatterns: [] }),
}),
{ name: 'composer-storage' }
)
);

Migration Considerations

From Pages Router to App Router

// Incremental migration path:
const migrationSteps = [
"1. Keep pages/ and add app/ directory",
"2. Move layout to app/layout.tsx",
"3. Migrate pages one by one",
"4. Convert getServerSideProps to Server Components",
"5. Add loading.tsx and error.tsx",
"6. Remove pages/ when complete"
];

From Create React App to Next.js

const migrationSteps = [
"1. Create new Next.js project",
"2. Copy components, hooks, utils",
"3. Move pages to app/ directory",
"4. Update routing (useNavigate → useRouter)",
"5. Convert data fetching to Server Components",
"6. Update environment variables (REACT_APP_ → NEXT_PUBLIC_)"
];

Performance Checklist

  • Server Components for static content
  • Client Components only when needed (interactivity)
  • Image optimization with next/image
  • Font optimization with next/font
  • Code splitting with dynamic imports
  • ISR or SSG for cacheable content
  • Edge Runtime for middleware