Agent skill

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.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/linter-driven-development

SKILL.md

Component 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:

  1. Pure feature-based → Continue pattern, implement as src/features/[new-feature]/
  2. Pure technical layers → Propose: Start migration with docs/architecture/feature-based-migration.md, implement new feature as first feature slice
  3. 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:

markdown
# 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)

typescript
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)

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
typescript
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
typescript
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:

typescript
// 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:

typescript
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.

Didn't find tool you were looking for?

Be as detailed as possible for better results