Claude-skill-registry component-designing
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. Focuses on feature-based architecture and type safety.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/linter-driven-development" ~/.claude/skills/majiayu000-claude-skill-registry-component-designing-613edc && rm -rf "$T"
skills/data/linter-driven-development/SKILL.mdComponent Designing
Component and type design for TypeScript + React applications. Use when planning new features or identifying need for new abstractions during refactoring.
When to Use
- Planning a new feature (before writing code)
- Refactoring reveals need for new components/hooks
- Linter failures suggest better abstractions
- When you need to think through component architecture
- Designing state management approach
Purpose
Design clean, well-composed components and types that:
- Prevent primitive obsession (use branded types, Zod schemas)
- Ensure type safety with TypeScript
- Follow component composition patterns
- Implement feature-based architecture
- Create reusable custom hooks
Workflow
0. Architecture Pattern Analysis (FIRST STEP)
Default: Always use feature-based architecture (group by feature, not technical layer).
Scan codebase structure:
- Feature-based:
✅src/features/auth/{LoginForm,useAuth,types,AuthContext}.tsx - Technical layers:
⚠️src/{components,hooks,contexts}/auth.tsx
Decision Flow:
- Pure feature-based → Continue pattern, implement as
src/features/[new-feature]/ - Pure technical layers → Propose: Start migration with
, implement new feature as first feature slicedocs/architecture/feature-based-migration.md - Mixed (migrating) → Check for migration docs, continue pattern as feature-based
Always ask user approval with options:
- Option A: Feature-based (recommended for cohesion/maintainability)
- Option B: Match existing pattern (if time-constrained)
- Acknowledge: Time pressure, team decisions, consistency needs are valid
If migration needed, create/update
docs/architecture/feature-based-migration.md:
# Feature-Based Architecture Migration Plan ## Current State: [technical-layers/mixed] ## Target: Feature-based structure in src/features/[feature]/ ## Strategy: New features feature-based, migrate existing incrementally ## Progress: [x] [new-feature] (this PR), [ ] existing features
See reference.md section #2 for detailed patterns.
1. Understand Domain
- What is the problem domain?
- What are the main UI concepts/interactions?
- What state needs to be managed?
- What are the user flows?
- How does this fit into existing architecture?
2. Identify Core Abstractions
Ask for each concept:
- Is this currently a primitive (string, number, boolean)?
- Does it have validation rules?
- Is it a UI concept (component)?
- Is it reusable logic (custom hook)?
- Is it shared state (context)?
- Does it need type safety (branded type)?
3. Design Self-Validating Types
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:
- Zod: Form validation, API parsing, runtime validation
- Branded types: Type safety without runtime overhead
4. Design Component Structure
Component Types:
A. Presentational Components (Pure UI)
- No state management
- Props-driven
- Reusable across features
- 100% testable
interface ButtonProps { label: string onClick: () => void variant?: 'primary' | 'secondary' disabled?: boolean } export function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) { return ( <button className={`btn btn-${variant}`} onClick={onClick} disabled={disabled} > {label} </button> ) }
B. Container Components (Logic + State)
- Manage state
- Handle side effects
- Coordinate data fetching
- Compose presentational components
export function LoginContainer() { const { login, isLoading, error } = useAuth() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const handleSubmit = async () => { try { const validEmail = EmailSchema.parse(email) await login(validEmail, password) } catch (error) { // Handle error } } return ( <LoginForm email={email} password={password} onEmailChange={setEmail} onPasswordChange={setPassword} onSubmit={handleSubmit} isLoading={isLoading} error={error} /> ) }
5. Design Custom Hooks
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 } }
6. Design Context for Shared State
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 }
7. Plan Feature Structure
Feature-based structure (Recommended):
src/features/auth/ ├── components/ │ ├── LoginForm.tsx # Presentational │ ├── LoginForm.test.tsx │ ├── RegisterForm.tsx │ └── RegisterForm.test.tsx ├── hooks/ │ ├── useAuth.ts # Custom hook │ ├── useAuth.test.ts │ ├── useFormValidation.ts │ └── useFormValidation.test.ts ├── context/ │ ├── AuthContext.tsx # Shared state │ └── AuthContext.test.tsx ├── types.ts # Email, UserId, etc. ├── api.ts # API calls └── index.ts # Public exports
Bad structure (Technical layers):
src/ ├── components/LoginForm.tsx ├── hooks/useAuth.ts ├── contexts/AuthContext.tsx └── types/auth.ts
8. Review Against Principles
Check design against (see reference.md):
- No primitive obsession (use Zod/branded types)
- Feature-based architecture
- Component composition over prop drilling
- Custom hooks for reusable logic
- Context only when needed (3+ levels)
- Clear separation: presentational vs container
- Props interfaces well-defined
Output Format
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/features/auth/ ├── components/ │ ├── LoginForm.tsx │ ├── LoginForm.test.tsx │ ├── RegisterForm.tsx │ └── RegisterForm.test.tsx ├── hooks/ │ ├── useAuth.ts │ ├── useAuth.test.ts │ ├── useFormValidation.ts │ └── useFormValidation.test.ts ├── context/ │ ├── AuthContext.tsx │ └── AuthContext.test.tsx ├── types.ts ├── api.ts └── index.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 presentational for reusability and testability - useAuth hook encapsulates auth logic for reuse across components - AuthContext provides auth state to avoid prop drilling - Feature-based structure keeps all auth code together 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 (Jest + RTL) 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.
Key Principles
See reference.md for detailed principles:
- Primitive obsession prevention (Zod schemas, branded types)
- Component composition patterns
- Feature-based architecture
- Custom hooks for reusable logic
- Context for shared state (use sparingly)
- Props interfaces and type safety
Pre-Code Review Questions
Before writing code, ask:
- Can logic be moved into custom hooks?
- Is this component presentational or container?
- Should state be local or context?
- Have I avoided primitive obsession?
- Is validation in the right place?
- Does this follow feature-based architecture?
- Are components small and focused?
Only after satisfactory answers, proceed to implementation.
See reference.md for complete design principles and examples.