Component and type design for TypeScript + React code. Use when planning new features, designing components and custom hooks, preventing primitive obsession, or when refactoring reveals need for new abstractions. Supports layer-based and hybrid architecture patterns with type safety.
Component and type design for TypeScript + React applications. Use when planning new features or identifying need for new abstractions during refactoring.
Design clean, well-composed components and types that:
Default: Match existing codebase architecture (consistency is key).
Scan codebase structure:
src/{components,hooks,contexts,types}/... - Group by technical layersrc/{components,hooks}/... + src/pages/... - Shared layers + page-specific codesrc/features/[feature]/{components,hooks,types} - Group by featureDecision Flow:
src/features/[new-feature]/Layer-Based Structure (Recommended for most codebases):
src/
components/ # Reusable components only
Button.tsx
Input.tsx
Modal.tsx
Card.tsx
pages/ # Top-level views/pages (use components)
LoginPage.tsx
DashboardPage.tsx
UserProfilePage.tsx
hooks/ # Reusable hooks
useAuth.ts
useDebounce.ts
useLocalStorage.ts
contexts/ # Shared context providers
AuthContext.tsx
ThemeContext.tsx
types/ # Shared type definitions
auth.ts
user.ts
api/ # API client
authApi.ts
userApi.ts
Key Distinction:
components/ = Reusable UI components (Button, Input, Modal)pages/ or views/ = Top-level page components that compose reusable componentsHybrid Structure (Common in practice):
src/
components/ # Truly shared UI components
Button.tsx
Input.tsx
hooks/ # Truly shared hooks
useDebounce.ts
pages/ # Pages with co-located feature-specific code
auth/
LoginPage.tsx
components/LoginForm.tsx
hooks/useLoginForm.ts
dashboard/
DashboardPage.tsx
components/StatsWidget.tsx
Key Principle: Consistency over dogma. Match the existing structure unless there's a compelling reason to change.
See reference.md section #2 for detailed patterns.
Ask for each concept:
For primitives with validation (Email, UserId, Port):
Option A: Zod Schemas (Recommended)
import { z } from 'zod'
// Schema definition with validation
export const EmailSchema = z.string().email().min(1)
export const UserIdSchema = z.string().uuid()
// Extract type from schema
export type Email = z.infer<typeof EmailSchema>
export type UserId = z.infer<typeof UserIdSchema>
// Validation function
export function validateEmail(value: unknown): Email {
return EmailSchema.parse(value) // Throws on invalid
}
Option B: Branded Types (TypeScript)
// Brand for nominal typing
declare const __brand: unique symbol
type Brand<T, TBrand> = T & { [__brand]: TBrand }
export type Email = Brand<string, 'Email'>
export type UserId = Brand<string, 'UserId'>
// Validating constructor
export function createEmail(value: string): Email {
if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
throw new Error('Invalid email format')
}
return value as Email
}
export function createUserId(value: string): UserId {
if (!value || value.length === 0) {
throw new Error('UserId cannot be empty')
}
return value as UserId
}
When to use which:
Component Types:
A. Presentational Components (Pure UI)
interface ButtonProps {
readonly label: string
readonly onClick: () => void
readonly disabled?: boolean
readonly variant?: 'primary' | 'secondary'
}
export function Button({
label,
onClick,
variant = 'primary',
disabled = false
}: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
onClick={onClick}
>
{label}
</button>
)
}
B. Container Components (Logic + State)
import { EMPTY_STRING } from 'consts'
export function LoginContainer() {
const { login, isLoading, error } = useAuth()
const [email, setEmail] = useState(EMPTY_STRING)
const [password, setPassword] = useState(EMPTY_STRING)
const handleSubmit = async () => {
try {
const validEmail = EmailSchema.parse(email)
await login(validEmail, password)
} catch (error) {
// Handle error
}
}
return (
<LoginForm
email={email}
error={error}
isLoading={isLoading}
password={password}
onEmailChange={setEmail}
onPasswordChange={setPassword}
onSubmit={handleSubmit}
/>
)
}
Extract reusable logic into custom hooks:
// Single responsibility: Form state management
export function useFormState<T>(initialValues: T) {
const [values, setValues] = useState<T>(initialValues)
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
const setValue = <K extends keyof T>(key: K, value: T[K]) => {
setValues(prev => ({ ...prev, [key]: value }))
setErrors(prev => ({ ...prev, [key]: undefined }))
}
const reset = () => {
setValues(initialValues)
setErrors({})
}
return { values, errors, setValue, setErrors, reset }
}
// Single responsibility: Data fetching
export function useUsers() {
const [users, setUsers] = useState<User[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const fetchUsers = async () => {
setIsLoading(true)
try {
const data = await api.getUsers()
setUsers(data)
} catch (err) {
setError(err as Error)
} finally {
setIsLoading(false)
}
}
fetchUsers()
}, [])
return { users, isLoading, error }
}
When state is needed across 3+ component levels:
interface AuthContextValue {
user: User | null
login: (email: Email, password: string) => Promise<void>
logout: () => Promise<void>
isAuthenticated: boolean
}
const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const login = async (email: Email, password: string) => {
const user = await api.login(email, password)
setUser(user)
}
const logout = async () => {
await api.logout()
setUser(null)
}
const value = useMemo(
() => ({ user, login, logout, isAuthenticated: !!user }),
[user]
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
Layer-based structure (most common):
src/
├── components/
│ ├── LoginForm.tsx
│ ├── LoginForm.test.tsx
│ ├── RegisterForm.tsx
│ └── RegisterForm.test.tsx
├── hooks/
│ ├── useAuth.ts
│ ├── useAuth.test.ts
│ ├── useFormValidation.ts
│ └── useFormValidation.test.ts
├── contexts/
│ ├── AuthContext.tsx
│ └── AuthContext.test.tsx
├── types/
│ └── auth.ts # Email, UserId, etc.
└── api/
└── authApi.ts # API calls
Hybrid structure (pages + shared layers):
src/
├── components/ # Shared components
│ ├── Button.tsx
│ └── Input.tsx
├── hooks/ # Shared hooks
│ └── useDebounce.ts
├── pages/
│ └── auth/
│ ├── LoginPage.tsx
│ ├── components/
│ │ └── LoginForm.tsx # Page-specific
│ └── hooks/
│ └── useLoginForm.ts # Page-specific
└── types/
└── auth.ts
Feature-based structure (alternative):
src/features/auth/
├── components/
│ ├── LoginForm.tsx
│ └── RegisterForm.tsx
├── hooks/
│ ├── useAuth.ts
│ └── useFormValidation.ts
├── context/
│ └── AuthContext.tsx
├── types.ts
└── api.ts
Choose the structure that matches your existing codebase.
Check design against (see reference.md):
After design phase:
🎨 DESIGN PLAN
Feature: User Authentication
Core Domain Types:
✅ Email (Zod schema) - RFC 5322 validation, used in login/register
✅ UserId (branded type) - Non-empty string, prevents invalid IDs
✅ User (interface) - { id: UserId, email: Email, name: string }
Components:
✅ LoginForm (Presentational)
Props: { email, password, onSubmit, isLoading, error }
Responsibility: UI only, no state
✅ LoginContainer (Container)
Responsibility: State management, form handling, validation
Uses: LoginForm, useAuth hook
✅ RegisterForm (Presentational)
Props: { formData, onSubmit, isLoading, errors }
Responsibility: UI only, no state
Custom Hooks:
✅ useAuth
Returns: { user, login, logout, isAuthenticated, isLoading }
Responsibility: Auth operations and state
✅ useFormValidation
Returns: { values, errors, setValue, validate, reset }
Responsibility: Form state and validation logic
Context:
✅ AuthContext
Provides: { user, login, logout, isAuthenticated }
Used by: Protected routes, user menu, profile pages
Reason: Auth state needed across entire app
Feature Structure:
📁 src/
├── components/ # Reusable components
│ ├── LoginForm.tsx
│ ├── LoginForm.test.tsx
│ ├── RegisterForm.tsx
│ └── RegisterForm.test.tsx
├── pages/ # Top-level pages
│ ├── LoginPage.tsx
│ └── RegisterPage.tsx
├── hooks/ # Reusable hooks
│ ├── useAuth.ts
│ ├── useAuth.test.ts
│ ├── useFormValidation.ts
│ └── useFormValidation.test.ts
├── contexts/ # Shared contexts
│ ├── AuthContext.tsx
│ └── AuthContext.test.tsx
├── types/ # Type definitions
│ └── auth.ts
└── api/ # API client
└── authApi.ts
Design Decisions:
- Email and UserId as validated types prevent runtime errors
- Zod for Email (form validation), branded type for UserId (type safety)
- LoginForm is reusable component, LoginPage composes it
- useAuth hook encapsulates auth logic for reuse across components
- AuthContext provides auth state to avoid prop drilling
- Layer-based structure: components/ for reusable, pages/ for top-level views
Integration Points:
- Consumed by: App routes, protected route wrapper, user menu
- Depends on: API client, token storage
- Events: User login/logout events for analytics
Next Steps:
1. Create types with validation (Zod schemas + branded types)
2. Write tests for types and hooks (React Testing Library)
3. Implement presentational components (LoginForm)
4. Implement container components (LoginContainer)
5. Add context provider (AuthContext)
6. Integration tests for full flows
Ready to implement? Use @testing skill for test structure (works with Jest, Vitest, etc.).
See reference.md for detailed principles:
Before writing code, ask:
Only after satisfactory answers, proceed to implementation.
All criteria must be met before design is considered complete.
Architecture Consistency
Type Safety
any types in designComponent Structure
readonly where appropriateCustom Hooks
Design Documentation
✅ COMPONENT DESIGN ACCEPTANCE CRITERIA
Architecture:
[ ] Existing pattern identified (layer/feature/hybrid)
[ ] New code follows existing pattern
[ ] File structure planned consistently
Type Safety:
[ ] Domain types defined (no primitive obsession)
[ ] Zod schemas for validation
[ ] Branded types for IDs
[ ] No any types
Components:
[ ] Presentational vs container distinction clear
[ ] Single responsibility
[ ] Props interfaces defined
[ ] No prop drilling planned
Hooks:
[ ] Reusable logic identified for extraction
[ ] Single responsibility per hook
[ ] Return types defined
Documentation:
[ ] Design plan complete
[ ] Integration points documented
[ ] Ready for implementation
Design complete: All boxes checked ✅
The following will BLOCK design completion:
Design must include: