Maintain consistent file structure - components/ui for base shadcn, components/shared for composed components, pages for routes, always use barrel exports
This guide establishes the file structure and naming conventions for maintaining a consistent, scalable React TypeScript codebase.
src/
├── assets/ # Static assets (images, icons, fonts)
│ ├── icons/
│ └── images/
├── components/ # Reusable components
│ ├── ui/ # Base shadcn components (CLI-generated)
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── badge.tsx
│ │ └── index.ts # Barrel export
│ ├── shared/ # Composed app-specific components
│ │ ├── UserProfileCard.tsx
│ │ ├── DataTable.tsx
│ │ └── index.ts # Barrel export
│ └── index.ts # Root barrel export
├── pages/ # Top-level route components
│ ├── Home.tsx
│ ├── About.tsx
│ ├── NotFound.tsx
│ └── index.ts # Barrel export
├── layouts/ # Layout wrappers
│ ├── MainLayout.tsx
│ ├── DashboardLayout.tsx
│ └── index.ts # Barrel export
├── features/ # Feature-based organization (optional)
│ ├── authentication/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── index.ts
│ └── dashboard/
│ ├── components/
│ ├── hooks/
│ └── index.ts
├── hooks/ # Custom React hooks
│ ├── useLocalStorage.ts
│ └── index.ts # Barrel export
├── lib/ # Utilities and helpers
│ └── utils.ts # cn() helper for Tailwind
├── services/ # API and external services
│ ├── api.service.ts
│ └── index.ts # Barrel export
├── constants/ # App-wide constants
│ └── index.ts # APP_NAME, ROUTES, etc.
├── types/ # TypeScript type definitions
│ ├── api.types.ts
│ ├── common.types.ts
│ └── index.ts # Barrel export
└── utils/ # Pure utility functions
└── index.ts
components/ui/Purpose: Base shadcn/ui components generated via CLI
npx shadcn@latest add [component]Example:
// components/ui/button.tsx (generated by shadcn CLI)
import * as React from "react"
import { cn } from "@/lib/utils"
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
components/shared/Purpose: Composed, app-specific components built from base UI components
ui/ componentsExample:
// components/shared/UserProfileCard.tsx
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
export interface UserProfileCardProps {
name: string;
role: string;
avatar: string;
}
export function UserProfileCard({ name, role, avatar }: UserProfileCardProps) {
return (
<Card>
<CardHeader>
<img src={avatar} alt={name} className="h-16 w-16 rounded-full" />
<CardTitle>{name}</CardTitle>
<Badge>{role}</Badge>
</CardHeader>
<CardContent>
{/* Additional user details */}
</CardContent>
</Card>
);
}
pages/Purpose: Top-level route components that render at specific URLs
Example:
// pages/Home.tsx
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { APP_NAME } from '@/constants';
export function Home() {
const features = [
{
title: 'Vite',
badge: 'Fast',
description: 'Lightning-fast build tool with HMR.',
gradient: 'from-violet-500 to-purple-500',
},
// ... more features
];
return (
<div className="space-y-32">
{/* Hero Section */}
<div className="relative overflow-hidden">
<h1 className="text-6xl font-extrabold">{APP_NAME}</h1>
<p className="text-xl text-gray-600">
A modern React TypeScript starter
</p>
</div>
{/* Features Grid */}
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{features.map((feature) => (
<Card key={feature.title}>
<CardHeader>
<CardTitle>{feature.title}</CardTitle>
<Badge>{feature.badge}</Badge>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
</Card>
))}
</div>
</div>
);
}
layouts/Purpose: Wrapper components that provide consistent structure across pages
<Outlet /> from react-router for nested routingExample:
// layouts/MainLayout.tsx
import { Link, Outlet, useLocation } from 'react-router-dom';
export function MainLayout() {
const location = useLocation();
const isActive = (path: string) => location.pathname === path;
return (
<div className="min-h-screen bg-white">
{/* Navigation Header */}
<header className="sticky top-0 z-50 border-b bg-white/95">
<nav className="mx-auto max-w-7xl px-8 py-4">
<div className="flex items-center gap-8">
<Link
to="/"
className={`px-4 py-2 font-medium ${
isActive('/') ? 'text-blue-600' : 'text-gray-600'
}`}
>
Home
</Link>
<Link
to="/about"
className={`px-4 py-2 font-medium ${
isActive('/about') ? 'text-blue-600' : 'text-gray-600'
}`}
>
About
</Link>
</div>
</nav>
</header>
{/* Main Content */}
<main className="mx-auto max-w-7xl px-8 py-16">
<Outlet />
</main>
</div>
);
}
lib/Purpose: Core utilities and helper functions
cn() helper for Tailwind class mergingutils/ instead)Example:
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
constants/Purpose: Application-wide constants and configuration
as const for type safetyExample:
// constants/index.ts
export const APP_NAME = 'Vite React TypeScript Boilerplate';
export const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api';
export const ROUTES = {
HOME: '/',
ABOUT: '/about',
NOT_FOUND: '*',
} as const;
export const STORAGE_KEYS = {
AUTH_TOKEN: 'auth_token',
USER_PREFERENCES: 'user_preferences',
} as const;
Always create index.ts files to enable clean imports and maintain a clear public API.
// components/ui/index.ts
export { Button } from './button';
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
} from './card';
export { Badge } from './badge';
export { Separator } from './separator';
// Only use when all exports from a module should be public
export * from './button';
export * from './card';
// components/index.ts - Exposes nested directories
export { Button } from './ui/button';
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
} from './ui/card';
Benefits:
import { Button, Card } from '@/components'Format: PascalCase
Button.tsx, UserProfileCard.tsx, MainLayout.tsxexport function Button(), export function UserProfileCard()Format: camelCase or kebab-case
utils.ts, api.service.ts, useLocalStorage.tsexport function cn(), export function useLocalStorage()Format: kebab-case
components/ui, components/shared, pages, layoutsFormat: SCREAMING_SNAKE_CASE
APP_NAME, API_BASE_URL, STORAGE_KEYSFormat: PascalCase with descriptive suffix
UserProfileCardProps, ApiResponse, AuthState┌─────────────────────────────────────┐
│ Need to create a new component? │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Is it a shadcn/ui base component? │
└──────────────┬──────────────────────┘
│
┌─────┴─────┐
│ │
YES NO
│ │
▼ ▼
┌─────────┐ ┌──────────────────────────────────┐
│ Use CLI │ │ Is it used in 2+ different │
│ to add │ │ features/pages? │
│ to ui/ │ └──────────────┬───────────────────┘
└─────────┘ │
┌─────┴─────┐
│ │
YES NO
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────────┐
│ Does it compose │ │ Is it a full page │
│ multiple ui/ │ │ route? │
│ components? │ └──────────┬──────────┘
└────────┬─────────┘ │
│ ┌────────┴────────┐
┌─────┴─────┐ │ │
│ │ YES NO
YES NO │ │
│ │ ▼ ▼
▼ ▼ ┌────────┐ ┌──────────────┐
┌──────────┐ ┌────────┐│ pages/ │ │ Is it part │
│ shared/ │ │ hooks/ ││ │ │ of a cohesive│
│ │ │ or │└────────┘ │ feature? │
└──────────┘ │ utils/ │ └──────┬───────┘
└────────┘ │
┌────────┴────────┐
│ │
YES NO
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ features/ │ │ Colocate with│
│ [feature]/ │ │ parent │
│ components/ │ │ component │
└──────────────┘ └──────────────┘
If you're copying the same JSX structure more than twice, extract it into a component.
Before:
// Multiple pages with duplicated card structure
export function Dashboard() {
return (
<Card>
<CardHeader>
<CardTitle>Stats</CardTitle>
</CardHeader>
<CardContent>{/* ... */}</CardContent>
</Card>
);
}
export function Profile() {
return (
<Card>
<CardHeader>
<CardTitle>User Info</CardTitle>
</CardHeader>
<CardContent>{/* ... */}</CardContent>
</Card>
);
}
After:
// components/shared/StatsCard.tsx
export function StatsCard({ title, children }) {
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
);
}
If a component or section exceeds 50 lines, consider extracting it.
Before:
export function Dashboard() {
return (
<div>
{/* 80 lines of user profile UI */}
<div className="user-profile">
{/* Complex profile rendering */}
</div>
{/* 60 lines of statistics UI */}
<div className="statistics">
{/* Complex stats rendering */}
</div>
</div>
);
}
After:
export function Dashboard() {
return (
<div>
<UserProfile />
<Statistics />
</div>
);
}
When combining 3+ base UI components, create a composed component.
// components/shared/UserCard.tsx - Composes Card, Badge, Button
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
export function UserCard({ user }) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{user.name}</CardTitle>
<Badge>{user.role}</Badge>
</div>
</CardHeader>
<CardContent>
<p>{user.email}</p>
<Button variant="outline">View Profile</Button>
</CardContent>
</Card>
);
}
When adding business logic, API calls, or state management, extract to a component.
// components/shared/ProductList.tsx - Contains domain logic
import { useEffect, useState } from 'react';
import { fetchProducts } from '@/services/api.service';
import { Card } from '@/components/ui/card';
export function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetchProducts().then(setProducts);
}, []);
return (
<div className="grid gap-4">
{products.map(product => (
<Card key={product.id}>
{/* Product display */}
</Card>
))}
</div>
);
}
// src/components/ui/index.ts
export { Button } from './button';
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
} from './card';
export { Badge } from './badge';
export { Separator } from './separator';
// src/pages/Home.tsx
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { APP_NAME } from '@/constants';
export function Home() {
// Page orchestrates UI components
return (
<div className="space-y-32">
<h1>{APP_NAME}</h1>
{/* Feature showcase using composed components */}
</div>
);
}
// src/layouts/MainLayout.tsx
import { Link, Outlet, useLocation } from 'react-router-dom';
export function MainLayout() {
const location = useLocation();
return (
<div className="min-h-screen">
<header className="sticky top-0">
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
</header>
<main>
<Outlet /> {/* Pages render here */}
</main>
</div>
);
}
// src/constants/index.ts
export const APP_NAME = 'Vite React TypeScript Boilerplate';
export const ROUTES = {
HOME: '/',
ABOUT: '/about',
NOT_FOUND: '*',
} as const;
// Usage in pages
import { ROUTES } from '@/constants';
<Link to={ROUTES.HOME}>Home</Link>
index.ts for new directory?components/ui/?components/shared/?pages/ directory?layouts/ directory?@/ path alias for imports?DON'T modify shadcn UI components directly:
// ❌ Bad: Editing ui/button.tsx manually
// If you need customization, create a wrapper in shared/
DON'T skip barrel exports:
// ❌ Bad: Direct imports without barrel
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
// ✅ Good: Use barrel exports
import { Button, Card } from '@/components/ui';
DON'T create one-off components in shared/:
// ❌ Bad: Single-use component in shared/
// components/shared/HomepageHeroSection.tsx (only used once)
// ✅ Good: Keep in the page or feature
// pages/Home.tsx (inline) or features/homepage/components/
DON'T mix concerns in constants:
// ❌ Bad: Mixing types and runtime values
export const ENDPOINTS = {
users: '/api/users',
auth: '/api/auth',
};
export type User = { id: string; name: string };
// ✅ Good: Separate constants and types
// constants/index.ts - runtime values
// types/index.ts - type definitions
Remember: Good organization makes scaling easy. When in doubt, follow the principle: "Start simple, extract when you repeat."