Strongtie Design System
Getting StartedComponents

Command Palette

Search for a command to run...

Getting Started
  • Introduction
  • Setup Guide
  • Package Installation
  • Code Quality Setup
  • Migration Guide
  • Resources
Registry
  • Getting Started
  • Combobox
  • Datepicker
  • MultiSelect
  • Tab Nav
  • Tree
Guides
  • Framework Recommendations
Foundations
  • States
  • Variables
Components
  • Accordion
  • Alert
  • Alert Dialog
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Command
  • Combobox
  • Context Menu
  • Date Picker
  • Dialog
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Item
  • Kbd
  • Label
  • Menubar
  • Multi Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Switch
  • Table
  • Tabs
  • Tab Nav
  • Textarea
  • Toaster
  • Toggle
  • Toggle Group
  • Tooltip
  • Tree
2026 Simpson Strong-Tie
  1. Docs
  2. Design Standards - Code Examples

Design Standards - Code Examples

PreviousNext

Comprehensive code examples referenced from the Design Standards documentation. Use these as templates when implementing features.

Comprehensive code examples referenced from the Design Standards documentation. Use these as templates when implementing features.

State Management

React Context - Session State

Use for auth, tenant selection, theme, and other session-level state.

lib/tenant-context.tsx
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
 
type Tenant = {
  readonly id: string;
  readonly name: string;
};
 
type TenantContextValue = {
  readonly tenants: Tenant[];
  readonly selectedTenant: Tenant | null;
  readonly setSelectedTenant: (tenant: Tenant) => void;
  readonly isLoading: boolean;
};
 
const TenantContext = createContext<TenantContextValue | null>(null);
 
export function TenantProvider({ children }: { readonly children: ReactNode }) {
  const [tenants, setTenants] = useState<Tenant[]>([]);
  const [selectedTenant, setSelectedTenant] = useState<Tenant | null>(null);
  const [isLoading, setIsLoading] = useState(true);
 
  // Load from localStorage on mount
  useEffect(() => {
    const savedTenantId = localStorage.getItem('selectedTenantId');
    // ... initialization logic
    setIsLoading(false);
  }, []);
 
  // Persist selection
  useEffect(() => {
    if (selectedTenant) {
      localStorage.setItem('selectedTenantId', selectedTenant.id);
    }
  }, [selectedTenant]);
 
  return (
    <TenantContext.Provider
      value={{ tenants, selectedTenant, setSelectedTenant, isLoading }}
    >
      {children}
    </TenantContext.Provider>
  );
}
 
export function useTenantContext() {
  const context = useContext(TenantContext);
  if (!context) {
    throw new Error('useTenantContext must be used within TenantProvider');
  }
  return context;
}

Zustand - Complex State

Use for cross-component state, computed values, or state needing middleware.

stores/project-filters-store.ts
import { create } from "zustand"
import { persist } from "zustand/middleware"
 
type ProjectStatus = "active" | "archived" | "draft"
type SortField = "name" | "createdAt" | "updatedAt"
type SortOrder = "asc" | "desc"
 
type ProjectFiltersState = {
  // State
  search: string
  statuses: ProjectStatus[]
  sortField: SortField
  sortOrder: SortOrder
 
  // Computed
  hasActiveFilters: () => boolean
 
  // Actions
  setSearch: (search: string) => void
  toggleStatus: (status: ProjectStatus) => void
  setSort: (field: SortField, order: SortOrder) => void
  resetFilters: () => void
}
 
const DEFAULT_STATE = {
  search: "",
  statuses: [] as ProjectStatus[],
  sortField: "updatedAt" as SortField,
  sortOrder: "desc" as SortOrder,
}
 
export const useProjectFiltersStore = create<ProjectFiltersState>()(
  persist(
    (set, get) => ({
      ...DEFAULT_STATE,
 
      hasActiveFilters: () => {
        const state = get()
        return state.search !== "" || state.statuses.length > 0
      },
 
      setSearch: (search) => set({ search }),
 
      toggleStatus: (status) =>
        set((state) => ({
          statuses: state.statuses.includes(status)
            ? state.statuses.filter((s) => s !== status)
            : [...state.statuses, status],
        })),
 
      setSort: (sortField, sortOrder) => set({ sortField, sortOrder }),
 
      resetFilters: () => set(DEFAULT_STATE),
    }),
    {
      name: "project-filters",
      partialize: (state) => ({
        sortField: state.sortField,
        sortOrder: state.sortOrder,
      }),
    }
  )
)

When NOT to Use Zustand

// BAD - Don't use Zustand for simple component state
const useModalStore = create((set) => ({
  isOpen: false,
  open: () => set({ isOpen: true }),
  close: () => set({ isOpen: false }),
}));
 
// GOOD - Use useState for component-local UI state
function ProjectCard() {
  const [isModalOpen, setIsModalOpen] = useState(false);
 
  return (
    <>
      <Button onClick={() => setIsModalOpen(true)}>Edit</Button>
      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
        {/* ... */}
      </Dialog>
    </>
  );
}

TanStack Query

Query Key Factory

hooks/query-keys.ts
export const queryKeys = {
  // Projects
  projects: {
    all: ["projects"] as const,
    lists: () => [...queryKeys.projects.all, "list"] as const,
    list: (filters: { tenantId?: string; status?: string }) =>
      [...queryKeys.projects.lists(), filters] as const,
    details: () => [...queryKeys.projects.all, "detail"] as const,
    detail: (id: string) => [...queryKeys.projects.details(), id] as const,
  },
 
  // Comments
  comments: {
    all: ["comments"] as const,
    byThread: (threadId: string) =>
      [...queryKeys.comments.all, "thread", threadId] as const,
    byProject: (projectId: string) =>
      [...queryKeys.comments.all, "project", projectId] as const,
  },
 
  // Users
  users: {
    all: ["users"] as const,
    current: () => [...queryKeys.users.all, "current"] as const,
    list: (filters?: { role?: string }) =>
      [...queryKeys.users.all, "list", filters] as const,
    detail: (id: string) => [...queryKeys.users.all, "detail", id] as const,
  },
 
  // Lookups (reference data)
  lookups: {
    all: ["lookups"] as const,
    projectTypes: () => [...queryKeys.lookups.all, "projectTypes"] as const,
    statuses: () => [...queryKeys.lookups.all, "statuses"] as const,
  },
}

Query Hook with Context Integration

hooks/use-projects.ts
import { useQuery } from "@tanstack/react-query"
 
import { projectsApi } from "@/lib/api/projects-api"
import { useTenantContext } from "@/lib/tenant-context"
 
import { queryKeys } from "./query-keys"
import { useAuthReady } from "./use-auth-ready"
 
export function useProjects() {
  const { selectedTenant } = useTenantContext()
  const isAuthReady = useAuthReady()
 
  return useQuery({
    queryKey: queryKeys.projects.list({ tenantId: selectedTenant?.id }),
    queryFn: projectsApi.getAll,
    enabled: isAuthReady && !!selectedTenant,
  })
}
 
export function useProject(id: string) {
  const isAuthReady = useAuthReady()
 
  return useQuery({
    queryKey: queryKeys.projects.detail(id),
    queryFn: () => projectsApi.getById(id),
    enabled: isAuthReady && !!id,
  })
}

Lookup Data Hook (Long Cache)

hooks/use-lookups.ts
import { keepPreviousData, useQuery } from "@tanstack/react-query"
 
import { lookupApi } from "@/lib/api/lookup-api"
 
import { queryKeys } from "./query-keys"
 
const LOOKUP_CONFIG = {
  staleTime: 30 * 60 * 1000, // 30 minutes
  gcTime: 60 * 60 * 1000, // 1 hour
  placeholderData: keepPreviousData,
  refetchOnWindowFocus: false,
  refetchOnMount: false,
}
 
export function useProjectTypes() {
  return useQuery({
    queryKey: queryKeys.lookups.projectTypes(),
    queryFn: lookupApi.getProjectTypes,
    ...LOOKUP_CONFIG,
  })
}
 
export function useStatuses() {
  return useQuery({
    queryKey: queryKeys.lookups.statuses(),
    queryFn: lookupApi.getStatuses,
    ...LOOKUP_CONFIG,
  })
}

Create Mutation with Optimistic Update

hooks/use-create-project.ts
import { useMutation, useQueryClient } from "@tanstack/react-query"
 
import {
  projectsApi,
  type CreateProjectRequest,
  type ProjectResponse,
} from "@/lib/api/projects-api"
import { getUserMessage } from "@/lib/error-messages"
import { useTenantContext } from "@/lib/tenant-context"
import { toast } from "@/hooks/use-toast"
 
import { queryKeys } from "./query-keys"
 
export function useCreateProject() {
  const queryClient = useQueryClient()
  const { selectedTenant } = useTenantContext()
 
  return useMutation({
    mutationFn: (data: CreateProjectRequest) => projectsApi.create(data),
 
    onMutate: async (newProject) => {
      // Cancel any outgoing refetches
      await queryClient.cancelQueries({
        queryKey: queryKeys.projects.lists(),
      })
 
      // Snapshot the previous value
      const previousProjects = queryClient.getQueryData<ProjectResponse[]>(
        queryKeys.projects.list({ tenantId: selectedTenant?.id })
      )
 
      // Optimistically update the cache
      queryClient.setQueryData<ProjectResponse[]>(
        queryKeys.projects.list({ tenantId: selectedTenant?.id }),
        (old) => [
          ...(old ?? []),
          {
            ...newProject,
            id: `temp-${Date.now()}`,
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString(),
          } as ProjectResponse,
        ]
      )
 
      // Return context with the snapshot
      return { previousProjects }
    },
 
    onSuccess: (data) => {
      toast({
        title: "Project created",
        description: `"${data.name}" has been created successfully.`,
      })
 
      // Invalidate to refetch with real data
      queryClient.invalidateQueries({
        queryKey: queryKeys.projects.lists(),
      })
    },
 
    onError: (error, _variables, context) => {
      // Rollback to the previous value
      if (context?.previousProjects) {
        queryClient.setQueryData(
          queryKeys.projects.list({ tenantId: selectedTenant?.id }),
          context.previousProjects
        )
      }
 
      toast({
        title: "Failed to create project",
        description: getUserMessage(error),
        variant: "destructive",
      })
    },
  })
}

Update Mutation with Optimistic Update

hooks/use-update-project.ts
import { useMutation, useQueryClient } from "@tanstack/react-query"
 
import {
  projectsApi,
  type ProjectResponse,
  type UpdateProjectRequest,
} from "@/lib/api/projects-api"
import { getUserMessage } from "@/lib/error-messages"
import { useTenantContext } from "@/lib/tenant-context"
import { toast } from "@/hooks/use-toast"
 
import { queryKeys } from "./query-keys"
 
export function useUpdateProject(projectId: string) {
  const queryClient = useQueryClient()
  const { selectedTenant } = useTenantContext()
 
  return useMutation({
    mutationFn: (data: UpdateProjectRequest) =>
      projectsApi.update(projectId, data),
 
    onMutate: async (updates) => {
      // Cancel related queries
      await queryClient.cancelQueries({
        queryKey: queryKeys.projects.detail(projectId),
      })
      await queryClient.cancelQueries({
        queryKey: queryKeys.projects.lists(),
      })
 
      // Snapshot previous values
      const previousProject = queryClient.getQueryData<ProjectResponse>(
        queryKeys.projects.detail(projectId)
      )
      const previousList = queryClient.getQueryData<ProjectResponse[]>(
        queryKeys.projects.list({ tenantId: selectedTenant?.id })
      )
 
      // Optimistically update detail cache
      if (previousProject) {
        queryClient.setQueryData<ProjectResponse>(
          queryKeys.projects.detail(projectId),
          {
            ...previousProject,
            ...updates,
            updatedAt: new Date().toISOString(),
          }
        )
      }
 
      // Optimistically update list cache
      if (previousList) {
        queryClient.setQueryData<ProjectResponse[]>(
          queryKeys.projects.list({ tenantId: selectedTenant?.id }),
          previousList.map((p) =>
            p.id === projectId
              ? { ...p, ...updates, updatedAt: new Date().toISOString() }
              : p
          )
        )
      }
 
      return { previousProject, previousList }
    },
 
    onSuccess: () => {
      toast({ title: "Project updated" })
 
      queryClient.invalidateQueries({
        queryKey: queryKeys.projects.detail(projectId),
      })
      queryClient.invalidateQueries({
        queryKey: queryKeys.projects.lists(),
      })
    },
 
    onError: (error, _variables, context) => {
      // Rollback
      if (context?.previousProject) {
        queryClient.setQueryData(
          queryKeys.projects.detail(projectId),
          context.previousProject
        )
      }
      if (context?.previousList) {
        queryClient.setQueryData(
          queryKeys.projects.list({ tenantId: selectedTenant?.id }),
          context.previousList
        )
      }
 
      toast({
        title: "Failed to update project",
        description: getUserMessage(error),
        variant: "destructive",
      })
    },
  })
}

Delete Mutation with Optimistic Update

hooks/use-delete-project.ts
import { useMutation, useQueryClient } from "@tanstack/react-query"
 
import { projectsApi, type ProjectResponse } from "@/lib/api/projects-api"
import { getUserMessage } from "@/lib/error-messages"
import { useTenantContext } from "@/lib/tenant-context"
import { toast } from "@/hooks/use-toast"
 
import { queryKeys } from "./query-keys"
 
export function useDeleteProject() {
  const queryClient = useQueryClient()
  const { selectedTenant } = useTenantContext()
 
  return useMutation({
    mutationFn: (projectId: string) => projectsApi.delete(projectId),
 
    onMutate: async (projectId) => {
      await queryClient.cancelQueries({
        queryKey: queryKeys.projects.lists(),
      })
 
      const previousList = queryClient.getQueryData<ProjectResponse[]>(
        queryKeys.projects.list({ tenantId: selectedTenant?.id })
      )
 
      // Optimistically remove from list
      queryClient.setQueryData<ProjectResponse[]>(
        queryKeys.projects.list({ tenantId: selectedTenant?.id }),
        (old) => old?.filter((p) => p.id !== projectId) ?? []
      )
 
      return { previousList }
    },
 
    onSuccess: (_data, projectId) => {
      toast({ title: "Project deleted" })
 
      // Remove from detail cache
      queryClient.removeQueries({
        queryKey: queryKeys.projects.detail(projectId),
      })
    },
 
    onError: (error, _projectId, context) => {
      if (context?.previousList) {
        queryClient.setQueryData(
          queryKeys.projects.list({ tenantId: selectedTenant?.id }),
          context.previousList
        )
      }
 
      toast({
        title: "Failed to delete project",
        description: getUserMessage(error),
        variant: "destructive",
      })
    },
  })
}

Prefetching on Hover

components/project-card.tsx
import { useQueryClient } from "@tanstack/react-query"
 
import { projectsApi } from "@/lib/api/projects-api"
import { queryKeys } from "@/hooks/query-keys"
 
function ProjectCard({ project }: { readonly project: ProjectResponse }) {
  const queryClient = useQueryClient()
 
  const handleMouseEnter = () => {
    // Prefetch project details on hover for instant navigation
    queryClient.prefetchQuery({
      queryKey: queryKeys.projects.detail(project.id),
      queryFn: () => projectsApi.getById(project.id),
      staleTime: 5 * 60 * 1000, // 5 minutes
    })
  }
 
  return (
    <Link to={`/projects/${project.id}`} onMouseEnter={handleMouseEnter}>
      <Card>
        <CardHeader>
          <CardTitle>{project.name}</CardTitle>
        </CardHeader>
      </Card>
    </Link>
  )
}

Forms

Simple Form (useState)

Use for 1-3 fields with no validation.

components/quick-search.tsx
import { useState } from "react"
import { Search } from "lucide-react"
 
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
 
export function QuickSearch({
  onSearch,
}: {
  readonly onSearch: (query: string) => void
}) {
  const [query, setQuery] = useState("")
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (query.trim()) {
      onSearch(query.trim())
    }
  }
 
  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <Input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
        aria-label="Search query"
      />
      <Button type="submit" size="icon" aria-label="Search">
        <Search className="h-4 w-4" />
      </Button>
    </form>
  )
}

Complex Form (React Hook Form + Zod)

Use for 4+ fields or when validation is required.

Step 1: Define Schema

schemas/project-schema.ts
import { z } from "zod"
 
export const createProjectSchema = z.object({
  name: z
    .string()
    .min(1, "Project name is required")
    .max(100, "Project name must be 100 characters or less"),
  type: z.enum(["internal", "external", "research"], {
    required_error: "Please select a project type",
  }),
  description: z
    .string()
    .max(500, "Description must be 500 characters or less")
    .optional(),
  startDate: z.date({
    required_error: "Start date is required",
  }),
  budget: z.number().min(0, "Budget must be a positive number").optional(),
  teamMembers: z
    .array(z.string())
    .min(1, "At least one team member is required"),
})
 
export type CreateProjectInput = z.infer<typeof createProjectSchema>
 
// Update schema (all fields optional for partial updates)
export const updateProjectSchema = createProjectSchema.partial()
export type UpdateProjectInput = z.infer<typeof updateProjectSchema>

Step 2: Create Form Component

components/create-project-form.tsx
import {
  createProjectSchema,
  type CreateProjectInput,
} from "@/schemas/project-schema"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
 
import { useCreateProject } from "@/hooks/use-create-project"
import { useProjectTypes } from "@/hooks/use-lookups"
import { Button } from "@/components/ui/button"
import { DatePicker } from "@/components/ui/date-picker"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { MultiSelect } from "@/components/ui/multi-select"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
 
type CreateProjectFormProps = {
  readonly onSuccess?: () => void
  readonly onCancel?: () => void
}
 
export function CreateProjectForm({
  onSuccess,
  onCancel,
}: CreateProjectFormProps) {
  const { mutate: createProject, isPending } = useCreateProject()
  const { data: projectTypes } = useProjectTypes()
 
  const form = useForm<CreateProjectInput>({
    resolver: zodResolver(createProjectSchema),
    defaultValues: {
      name: "",
      type: undefined,
      description: "",
      startDate: undefined,
      budget: undefined,
      teamMembers: [],
    },
  })
 
  const onSubmit = (data: CreateProjectInput) => {
    createProject(data, {
      onSuccess: () => {
        form.reset()
        onSuccess?.()
      },
    })
  }
 
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        {/* Text Input */}
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Project Name</FormLabel>
              <FormControl>
                <Input placeholder="Enter project name" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        {/* Select */}
        <FormField
          control={form.control}
          name="type"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Project Type</FormLabel>
              <Select onValueChange={field.onChange} defaultValue={field.value}>
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="Select a project type" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  {projectTypes?.map((type) => (
                    <SelectItem key={type.value} value={type.value}>
                      {type.label}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />
 
        {/* Textarea */}
        <FormField
          control={form.control}
          name="description"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Description</FormLabel>
              <FormControl>
                <Textarea
                  placeholder="Describe the project..."
                  className="resize-none"
                  {...field}
                />
              </FormControl>
              <FormDescription>
                Optional. Maximum 500 characters.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
 
        {/* Date Picker */}
        <FormField
          control={form.control}
          name="startDate"
          render={({ field }) => (
            <FormItem className="flex flex-col">
              <FormLabel>Start Date</FormLabel>
              <DatePicker date={field.value} onDateChange={field.onChange} />
              <FormMessage />
            </FormItem>
          )}
        />
 
        {/* Number Input */}
        <FormField
          control={form.control}
          name="budget"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Budget</FormLabel>
              <FormControl>
                <Input
                  type="number"
                  placeholder="0.00"
                  {...field}
                  onChange={(e) =>
                    field.onChange(e.target.valueAsNumber || undefined)
                  }
                />
              </FormControl>
              <FormDescription>Optional. Enter amount in USD.</FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
 
        {/* Multi-Select */}
        <FormField
          control={form.control}
          name="teamMembers"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Team Members</FormLabel>
              <FormControl>
                <MultiSelect
                  selected={field.value}
                  onChange={field.onChange}
                  placeholder="Select team members"
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        {/* Actions */}
        <div className="flex justify-end gap-3">
          {onCancel && (
            <Button type="button" variant="outline" onClick={onCancel}>
              Cancel
            </Button>
          )}
          <Button type="submit" disabled={isPending}>
            {isPending ? "Creating..." : "Create Project"}
          </Button>
        </div>
      </form>
    </Form>
  )
}

Error Handling

API Client with Error Handling

lib/api-client.ts
export class ApiError extends Error {
  constructor(
    message: string,
    public readonly status: number,
    public readonly code: string = "UNKNOWN_ERROR"
  ) {
    super(message)
    this.name = "ApiError"
  }
}
 
type ApiResponse<T> = {
  success: boolean
  message?: string
  data: T
  error?: {
    code: string
    message: string
  }
}
 
class ApiClient {
  private baseUrl: string
  private getAccessToken: (() => string | null) | null = null
 
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
  }
 
  setAccessTokenGetter(getter: () => string | null) {
    this.getAccessToken = getter
  }
 
  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = `${this.baseUrl}/${endpoint}`
    const token = this.getAccessToken?.()
 
    const headers: HeadersInit = {
      "Content-Type": "application/json",
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers,
    }
 
    try {
      const response = await fetch(url, { ...options, headers })
      const data: ApiResponse<T> = await response.json()
 
      if (!response.ok || !data.success) {
        throw new ApiError(
          data.error?.message ?? data.message ?? "Request failed",
          response.status,
          data.error?.code ?? "API_ERROR"
        )
      }
 
      return data.data
    } catch (error) {
      if (error instanceof ApiError) {
        throw error
      }
 
      // Network error
      throw new ApiError("Unable to connect to server", 0, "NETWORK_ERROR")
    }
  }
 
  get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: "GET" })
  }
 
  post<T>(endpoint: string, body: unknown): Promise<T> {
    return this.request<T>(endpoint, {
      method: "POST",
      body: JSON.stringify(body),
    })
  }
 
  put<T>(endpoint: string, body: unknown): Promise<T> {
    return this.request<T>(endpoint, {
      method: "PUT",
      body: JSON.stringify(body),
    })
  }
 
  delete<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: "DELETE" })
  }
}
 
export const api = new ApiClient(import.meta.env.VITE_API_BASE_URL)

Error Message Mapping

lib/error-messages.ts
import type { ApiError } from "./api-client"
 
const ERROR_MESSAGES: Record<string, string> = {
  // Authentication
  UNAUTHORIZED: "Your session has expired. Please sign in again.",
  FORBIDDEN: "You do not have permission to perform this action.",
 
  // Validation
  VALIDATION_ERROR: "Please check your input and try again.",
  INVALID_INPUT: "The provided data is invalid.",
 
  // Resources
  NOT_FOUND: "The requested item could not be found.",
  ALREADY_EXISTS: "An item with this name already exists.",
  CONFLICT: "This item has been modified. Please refresh and try again.",
 
  // Business logic
  TENANT_REQUIRED: "Please select a tenant first.",
  PROJECT_ARCHIVED: "This project is archived and cannot be modified.",
  LIMIT_EXCEEDED: "You have reached the maximum limit for this resource.",
 
  // Network
  NETWORK_ERROR: "Unable to connect. Please check your internet connection.",
  TIMEOUT: "The request timed out. Please try again.",
 
  // Server
  INTERNAL_ERROR: "An unexpected error occurred. Please try again later.",
  SERVICE_UNAVAILABLE: "The service is temporarily unavailable.",
 
  // Default
  DEFAULT: "Something went wrong. Please try again.",
}
 
export function getUserMessage(error: unknown): string {
  if (error instanceof Error && "code" in error) {
    const apiError = error as ApiError
    return ERROR_MESSAGES[apiError.code] ?? ERROR_MESSAGES.DEFAULT
  }
 
  if (error instanceof Error) {
    return error.message
  }
 
  return ERROR_MESSAGES.DEFAULT
}
 
// For specific error handling
export function isAuthError(error: unknown): boolean {
  if (error instanceof Error && "code" in error) {
    const code = (error as ApiError).code
    return code === "UNAUTHORIZED" || code === "FORBIDDEN"
  }
  return false
}
 
export function isNetworkError(error: unknown): boolean {
  if (error instanceof Error && "code" in error) {
    return (error as ApiError).code === "NETWORK_ERROR"
  }
  return false
}

Error Boundary Component

components/error-boundary.tsx
import { Component, type ReactNode } from "react"
import { AlertTriangle } from "lucide-react"
 
import { Button } from "@/components/ui/button"
 
type ErrorBoundaryProps = {
  readonly children: ReactNode
  readonly fallback?: ReactNode
}
 
type ErrorBoundaryState = {
  hasError: boolean
  error: Error | null
}
 
export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  constructor(props: ErrorBoundaryProps) {
    super(props)
    this.state = { hasError: false, error: null }
  }
 
  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error }
  }
 
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log to error reporting service
    console.error("Error caught by boundary:", error, errorInfo)
  }
 
  handleReset = () => {
    this.setState({ hasError: false, error: null })
  }
 
  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback
      }
 
      return (
        <div className="flex min-h-[400px] flex-col items-center justify-center p-8">
          <AlertTriangle className="text-destructive h-12 w-12" />
          <h2 className="mt-4 text-xl font-semibold">Something went wrong</h2>
          <p className="text-muted-foreground mt-2 text-center">
            An unexpected error occurred. Please try refreshing the page.
          </p>
          <div className="mt-6 flex gap-3">
            <Button variant="outline" onClick={() => window.location.reload()}>
              Refresh Page
            </Button>
            <Button onClick={this.handleReset}>Try Again</Button>
          </div>
        </div>
      )
    }
 
    return this.props.children
  }
}

Inline Error Component

components/error-message.tsx
import { AlertCircle, RefreshCw } from "lucide-react"
 
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
 
type ErrorMessageProps = {
  readonly title?: string
  readonly message: string
  readonly onRetry?: () => void
  readonly className?: string
}
 
export function ErrorMessage({
  title = "Error",
  message,
  onRetry,
  className,
}: ErrorMessageProps) {
  return (
    <div
      className={cn(
        "flex flex-col items-center justify-center py-8 text-center",
        className
      )}
    >
      <AlertCircle className="text-destructive h-12 w-12" />
      <h3 className="mt-4 text-lg font-semibold">{title}</h3>
      <p className="text-muted-foreground mt-2 max-w-md text-sm">{message}</p>
      {onRetry && (
        <Button variant="outline" onClick={onRetry} className="mt-4">
          <RefreshCw className="mr-2 h-4 w-4" />
          Try Again
        </Button>
      )}
    </div>
  )
}

Loading States

Skeleton Components

components/skeletons/project-card-skeleton.tsx
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
 
export function ProjectCardSkeleton() {
  return (
    <Card>
      <CardHeader className="space-y-2">
        <Skeleton className="h-6 w-3/4" />
        <Skeleton className="h-4 w-1/2" />
      </CardHeader>
      <CardContent className="space-y-3">
        <Skeleton className="h-4 w-full" />
        <Skeleton className="h-4 w-5/6" />
        <div className="flex gap-2 pt-2">
          <Skeleton className="h-6 w-16 rounded-full" />
          <Skeleton className="h-6 w-20 rounded-full" />
        </div>
      </CardContent>
    </Card>
  )
}
 
export function ProjectGridSkeleton({
  count = 6,
}: {
  readonly count?: number
}) {
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      {Array.from({ length: count }).map((_, i) => (
        <ProjectCardSkeleton key={i} />
      ))}
    </div>
  )
}
components/skeletons/table-skeleton.tsx
import { Skeleton } from "@/components/ui/skeleton"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
 
type TableSkeletonProps = {
  readonly columns: number
  readonly rows?: number
}
 
export function TableSkeleton({ columns, rows = 5 }: TableSkeletonProps) {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          {Array.from({ length: columns }).map((_, i) => (
            <TableHead key={i}>
              <Skeleton className="h-4 w-24" />
            </TableHead>
          ))}
        </TableRow>
      </TableHeader>
      <TableBody>
        {Array.from({ length: rows }).map((_, rowIndex) => (
          <TableRow key={rowIndex}>
            {Array.from({ length: columns }).map((_, colIndex) => (
              <TableCell key={colIndex}>
                <Skeleton className="h-4 w-full" />
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Empty State Component

components/empty-state.tsx
import type { LucideIcon } from "lucide-react"
 
import { cn } from "@/lib/utils"
 
type EmptyStateProps = {
  readonly icon: LucideIcon
  readonly title: string
  readonly description: string
  readonly action?: React.ReactNode
  readonly className?: string
}
 
export function EmptyState({
  icon: Icon,
  title,
  description,
  action,
  className,
}: EmptyStateProps) {
  return (
    <div
      className={cn(
        "flex flex-col items-center justify-center py-12 text-center",
        className
      )}
    >
      <div className="bg-muted rounded-full p-4">
        <Icon className="text-muted-foreground h-8 w-8" />
      </div>
      <h3 className="mt-4 text-lg font-semibold">{title}</h3>
      <p className="text-muted-foreground mt-2 max-w-sm text-sm">
        {description}
      </p>
      {action && <div className="mt-6">{action}</div>}
    </div>
  )
}

Complete Async Component Pattern

components/projects-list.tsx
import { FolderOpen, Plus } from "lucide-react"
 
import { getUserMessage } from "@/lib/error-messages"
import { useProjects } from "@/hooks/use-projects"
import { Button } from "@/components/ui/button"
import { EmptyState } from "@/components/empty-state"
import { ErrorMessage } from "@/components/error-message"
import { ProjectCard } from "@/components/project-card"
import { ProjectGridSkeleton } from "@/components/skeletons/project-card-skeleton"
 
export function ProjectsList() {
  const { data: projects, isLoading, error, refetch } = useProjects()
 
  // Loading state
  if (isLoading) {
    return <ProjectGridSkeleton count={6} />
  }
 
  // Error state
  if (error) {
    return (
      <ErrorMessage
        title="Failed to load projects"
        message={getUserMessage(error)}
        onRetry={refetch}
      />
    )
  }
 
  // Empty state
  if (!projects?.length) {
    return (
      <EmptyState
        icon={FolderOpen}
        title="No projects yet"
        description="Get started by creating your first project."
        action={
          <Button>
            <Plus className="mr-2 h-4 w-4" />
            Create Project
          </Button>
        }
      />
    )
  }
 
  // Success state
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      {projects.map((project) => (
        <ProjectCard key={project.id} project={project} />
      ))}
    </div>
  )
}

Accessibility

Icon Button Examples

// CORRECT - All icon buttons have accessible names
 
// Using aria-label (preferred)
<Button variant="ghost" size="icon" aria-label="Edit project">
  <Pencil className="h-4 w-4" />
</Button>
 
<Button variant="ghost" size="icon" aria-label="Delete project">
  <Trash2 className="h-4 w-4" />
</Button>
 
<Button variant="ghost" size="icon" aria-label="View notifications">
  <Bell className="h-4 w-4" />
</Button>
 
// Using sr-only span (alternative)
<Button variant="ghost" size="icon">
  <Settings className="h-4 w-4" />
  <span className="sr-only">Open settings</span>
</Button>
 
// With dynamic context
<Button
  variant="ghost"
  size="icon"
  aria-label={`Delete "${project.name}"`}
>
  <Trash2 className="h-4 w-4" />
</Button>
 
// Toggle button with state
<Button
  variant="ghost"
  size="icon"
  aria-label={isExpanded ? 'Collapse section' : 'Expand section'}
  aria-expanded={isExpanded}
>
  {isExpanded ? (
    <ChevronUp className="h-4 w-4" />
  ) : (
    <ChevronDown className="h-4 w-4" />
  )}
</Button>

Form Accessibility

// CORRECT - Accessible form with proper labels and error handling
 
<FormField
  control={form.control}
  name="email"
  render={({ field, fieldState }) => (
    <FormItem>
      <FormLabel>
        Email Address
        <span className="text-destructive" aria-hidden="true">
          {" "}
          *
        </span>
      </FormLabel>
      <FormControl>
        <Input
          type="email"
          placeholder="you@example.com"
          aria-required="true"
          aria-invalid={!!fieldState.error}
          aria-describedby={
            fieldState.error
              ? `${field.name}-error`
              : `${field.name}-description`
          }
          {...field}
        />
      </FormControl>
      <FormDescription id={`${field.name}-description`}>
        We'll never share your email with anyone.
      </FormDescription>
      <FormMessage id={`${field.name}-error`} />
    </FormItem>
  )}
/>

Keyboard Navigation

components/actions-menu.tsx
// CORRECT - Keyboard accessible dropdown menu
 
function ActionsMenu({ project }: { readonly project: Project }) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          variant="ghost"
          size="icon"
          aria-label={`Actions for ${project.name}`}
        >
          <MoreHorizontal className="h-4 w-4" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onSelect={() => handleEdit(project)}>
          <Pencil className="mr-2 h-4 w-4" />
          Edit
        </DropdownMenuItem>
        <DropdownMenuItem onSelect={() => handleDuplicate(project)}>
          <Copy className="mr-2 h-4 w-4" />
          Duplicate
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem
          onSelect={() => handleDelete(project)}
          className="text-destructive focus:text-destructive"
        >
          <Trash2 className="mr-2 h-4 w-4" />
          Delete
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Component Patterns

Standard Component Structure

components/project-card.tsx
import { useQueryClient } from "@tanstack/react-query"
import { Calendar, MoreHorizontal } from "lucide-react"
import { Link } from "react-router"
 
import { projectsApi, type ProjectResponse } from "@/lib/api/projects-api"
import { formatDate } from "@/lib/date-utils"
import { cn } from "@/lib/utils"
import { queryKeys } from "@/hooks/query-keys"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
 
// Props are readonly
type ProjectCardProps = {
  readonly project: ProjectResponse
  readonly className?: string
}
 
// Sub-component defined at module level (not nested)
function ProjectStatusBadge({ status }: { readonly status: string }) {
  const variants: Record<string, "default" | "secondary" | "destructive"> = {
    active: "default",
    draft: "secondary",
    archived: "destructive",
  }
 
  return <Badge variant={variants[status] ?? "secondary"}>{status}</Badge>
}
 
// Named export (not default)
export function ProjectCard({ project, className }: ProjectCardProps) {
  const queryClient = useQueryClient()
 
  // Prefetch on hover
  const handleMouseEnter = () => {
    queryClient.prefetchQuery({
      queryKey: queryKeys.projects.detail(project.id),
      queryFn: () => projectsApi.getById(project.id),
    })
  }
 
  return (
    <Card
      className={cn("transition-shadow hover:shadow-md", className)}
      onMouseEnter={handleMouseEnter}
    >
      <CardHeader className="flex flex-row items-start justify-between space-y-0">
        <div className="space-y-1">
          <CardTitle className="line-clamp-1">
            <Link
              to={`/projects/${project.id}`}
              className="focus:ring-ring hover:underline focus:ring-2 focus:outline-none"
            >
              {project.name}
            </Link>
          </CardTitle>
          <div className="text-muted-foreground flex items-center gap-2 text-sm">
            <Calendar className="h-3 w-3" aria-hidden="true" />
            <span>{formatDate(project.createdAt)}</span>
          </div>
        </div>
        <Button
          variant="ghost"
          size="icon"
          aria-label={`Actions for ${project.name}`}
        >
          <MoreHorizontal className="h-4 w-4" />
        </Button>
      </CardHeader>
      <CardContent>
        {project.description && (
          <p className="text-muted-foreground line-clamp-2 text-sm">
            {project.description}
          </p>
        )}
        <div className="mt-4 flex gap-2">
          <ProjectStatusBadge status={project.status} />
        </div>
      </CardContent>
    </Card>
  )
}

Performance

Lazy Loading Heavy Components

components/rich-text-editor.tsx
import { lazy, Suspense } from "react"
 
import { Skeleton } from "@/components/ui/skeleton"
 
// Lazy load the heavy editor component
const PlateEditor = lazy(() => import("@/components/plate-editor"))
 
type RichTextEditorProps = {
  readonly value: string
  readonly onChange: (value: string) => void
  readonly placeholder?: string
}
 
export function RichTextEditor(props: RichTextEditorProps) {
  return (
    <Suspense fallback={<Skeleton className="h-32 w-full" />}>
      <PlateEditor {...props} />
    </Suspense>
  )
}
pages/analytics-page.tsx
import { lazy, Suspense } from "react"
 
import { PageSkeleton } from "@/components/skeletons/page-skeleton"
 
const AnalyticsDashboard = lazy(
  () => import("@/components/analytics-dashboard")
)
 
export function AnalyticsPage() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <AnalyticsDashboard />
    </Suspense>
  )
}

Avoiding Performance Anti-Patterns

// BAD - Namespace import for large module
import * as dateFns from "date-fns"
// GOOD - Specific imports
import { differenceInDays, format, parseISO } from "date-fns"
 
// BAD - Spread in loop accumulator
const result = items.reduce(
  (acc, item) => ({
    ...acc,
    [item.id]: item,
  }),
  {}
)
 
// GOOD - Direct mutation in accumulator
const result = items.reduce(
  (acc, item) => {
    acc[item.id] = item
    return acc
  },
  {} as Record<string, Item>
)
 
// BAD - Creating regex in loop
items.filter((item) => new RegExp(searchTerm, "i").test(item.name))
 
// GOOD - Regex at top level
const searchRegex = new RegExp(searchTerm, "i")
items.filter((item) => searchRegex.test(item.name))
Contribution ModelDesign Standards

On This Page

State ManagementReact Context - Session StateZustand - Complex StateWhen NOT to Use ZustandTanStack QueryQuery Key FactoryQuery Hook with Context IntegrationLookup Data Hook (Long Cache)Create Mutation with Optimistic UpdateUpdate Mutation with Optimistic UpdateDelete Mutation with Optimistic UpdatePrefetching on HoverFormsSimple Form (useState)Complex Form (React Hook Form + Zod)Step 1: Define SchemaStep 2: Create Form ComponentError HandlingAPI Client with Error HandlingError Message MappingError Boundary ComponentInline Error ComponentLoading StatesSkeleton ComponentsEmpty State ComponentComplete Async Component PatternAccessibilityIcon Button ExamplesForm AccessibilityKeyboard NavigationComponent PatternsStandard Component StructurePerformanceLazy Loading Heavy ComponentsAvoiding Performance Anti-Patterns

Contribute

  • Report an issue
  • Request a feature
  • Edit this page