---
name: strongtie-react-practices
description: "Follow React best practices and web interface guidelines for accessibility, performance, forms, animation, and UX patterns. Use when: (1) Writing React components, (2) Reviewing code, (3) Implementing forms or data fetching."
---

# React & Web Interface Best Practices

Follow React best practices and Vercel Web Interface Guidelines for building high-quality UIs.

## When to Use This Skill

- Writing new React components
- Reviewing existing code
- Implementing forms or data fetching
- Optimizing performance
- Ensuring accessibility

## React Compiler (React 19+)

This project uses React Compiler (`babel-plugin-react-compiler`):

**DO NOT USE:**
- `useMemo()` - compiler handles memoization automatically
- `useCallback()` - compiler optimizes callback references
- `React.memo()` - rarely needed with compiler optimization

**DO USE:**
- `React.lazy()` for code splitting (unrelated to memoization)
- Standard hooks: `useState`, `useEffect`, `useRef`
- No `forwardRef` needed - ref is a regular prop in React 19

## State Management Decision Tree

1. **Is this server data (from API)?** → **TanStack Query**
2. **Is this component-local UI state?** → **useState**
3. **Is this session-level shared state (auth, tenant, theme)?** → **React Context**
4. **Is this complex, computed, or cross-component state?** → **Zustand**

### Anti-Patterns

- Never store API responses in `useState`
- Never duplicate TanStack Query cache in local state
- Never create Context for single-component state

## TanStack Query Patterns

### Query Key Factory

```typescript
// hooks/query-keys.ts
export const queryKeys = {
  projects: {
    all: ["projects"] as const,
    lists: () => [...queryKeys.projects.all, "list"] as const,
    list: (filters: ProjectFilters) =>
      [...queryKeys.projects.lists(), filters] as const,
    details: () => [...queryKeys.projects.all, "detail"] as const,
    detail: (id: string) => [...queryKeys.projects.details(), id] as const,
  },
}
```

### Custom Query Hooks

Never use `useQuery` directly in components:

```typescript
export function useProjects() {
  const { selectedTenant } = useTenantContext()
  const isAuthReady = useAuthReady()

  return useQuery({
    queryKey: queryKeys.projects.list({ tenantId: selectedTenant?.id }),
    queryFn: projectsApi.getAll,
    enabled: isAuthReady,
  })
}
```

### Optimistic Updates

```typescript
export function useCreateProject() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: projectsApi.create,
    onMutate: async (newProject) => {
      await queryClient.cancelQueries({ queryKey: queryKeys.projects.lists() })
      const previous = queryClient.getQueryData(queryKeys.projects.list({}))
      queryClient.setQueryData(queryKeys.projects.list({}), (old) => [
        ...(old ?? []),
        { ...newProject, id: `temp-${Date.now()}` },
      ])
      return { previous }
    },
    onError: (_err, _vars, context) => {
      queryClient.setQueryData(queryKeys.projects.list({}), context?.previous)
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: queryKeys.projects.lists() })
    },
  })
}
```

## Form Handling (React Hook Form + Zod)

### When to Use

| Condition                     | Solution              |
| ----------------------------- | --------------------- |
| 1-3 fields AND no validation  | `useState`            |
| 4+ fields OR validation rules | React Hook Form + Zod |

### Pattern

```typescript
const schema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
})

const form = useForm<z.infer<typeof schema>>({
  resolver: zodResolver(schema),
})
```

## Error Handling

### Architecture

| Error Type            | Handler                   |
| --------------------- | ------------------------- |
| React component crash | Error Boundary            |
| API error (expected)  | Query error state + Toast |
| Validation error      | Form field errors         |
| Network error         | Toast notification        |

### Error Message Pattern

- APIs return error codes, not user messages
- UI maps codes to user-friendly messages via `lib/error-messages.ts`
- Never display raw API error messages to users

## Accessibility Requirements

### Icon-Only Buttons

```typescript
// REQUIRED - use aria-label
<Button variant="ghost" size="icon" aria-label="Delete project">
  <Trash2 className="h-4 w-4" />
</Button>

// ALTERNATIVE - use sr-only span
<Button variant="ghost" size="icon">
  <Trash2 className="h-4 w-4" />
  <span className="sr-only">Delete project</span>
</Button>

// NEVER - no accessible name
<Button variant="ghost" size="icon">
  <Trash2 className="h-4 w-4" />
</Button>
```

### Required for All Components

- Form inputs must have labels
- Images must have meaningful alt text (or `alt=""` if decorative)
- Interactive elements must be keyboard accessible
- Focus states must be visible
- Color cannot be the only indicator of state

## Focus States

- Interactive elements need visible focus: `focus-visible:ring-*`
- Never `outline-none` without focus replacement
- Use `:focus-visible` over `:focus` (avoid focus ring on click)

## Forms (UI Patterns)

- Inputs need `autocomplete` and meaningful `name`
- Use correct `type` (`email`, `tel`, `url`, `number`) and `inputmode`
- Never block paste
- Labels clickable (`htmlFor` or wrapping control)
- Errors inline next to fields; focus first error on submit
- Warn before navigation with unsaved changes
- Disable spellcheck on emails, codes, usernames
- Submit button stays enabled until request starts; spinner during request

## Animation

- Honor `prefers-reduced-motion`
- Animate `transform`/`opacity` only (compositor-friendly)
- Never `transition: all`—list properties explicitly
- Animations interruptible—respond to user input mid-animation

## Typography

- `…` not `...`
- Curly quotes `"` `"` not straight `"`
- Non-breaking spaces: `10&nbsp;MB`, `⌘&nbsp;K`
- `font-variant-numeric: tabular-nums` for number columns
- Use `text-wrap: balance` on headings

## Performance

- Large lists (>50 items): virtualize
- No layout reads in render (`getBoundingClientRect`, `offsetHeight`)
- Prefer uncontrolled inputs; controlled inputs must be cheap per keystroke
- Images need explicit `width` and `height` (prevents CLS)
- Below-fold images: `loading="lazy"`
- Use Suspense for code splitting
- Implement loading states with Skeleton components

## Loading & Async States

### Use Skeleton when:
- Content layout is known/predictable
- Preventing layout shift matters
- Data typically loads in < 3 seconds

### Use Spinner when:
- Layout is unpredictable
- Action-triggered loading (button clicks)
- Overlay/blocking operations

### Always provide:
- Loading state (Skeleton or Spinner)
- Error state (with retry action)
- Empty state (with call-to-action)

## Navigation & State

- URL reflects state—filters, tabs, pagination in query params
- Links use `<a>`/`<Link>` (Cmd/Ctrl+click support)
- Destructive actions need confirmation or undo window

## Touch & Interaction

- `touch-action: manipulation` (prevents double-tap zoom delay)
- `overscroll-behavior: contain` in modals/drawers/sheets
- `autoFocus` sparingly—desktop only, single primary input

## Dark Mode

- `color-scheme: dark` on `<html>` for dark themes
- `<meta name="theme-color">` matches page background

## Anti-patterns (Avoid)

- `user-scalable=no` disabling zoom
- `transition: all`
- `outline-none` without focus-visible replacement
- `<div>` with click handlers (should be `<button>`)
- Images without dimensions
- Form inputs without labels
- Icon buttons without `aria-label`
- Hardcoded date/number formats (use `Intl.*`)

## Content & Copy

- Active voice: "Install the CLI" not "The CLI will be installed"
- Title Case for headings/buttons
- Specific button labels: "Save API Key" not "Continue"
- Error messages include fix/next step

## Component Checklist

Before completing any component:

- [ ] Named export (not default)
- [ ] Props use `readonly` modifier
- [ ] Sub-components defined at module level (not nested)
- [ ] Loading state handled (Skeleton)
- [ ] Error state handled (with retry)
- [ ] Empty state handled (with CTA)
- [ ] Icon buttons have `aria-label`
- [ ] Form inputs have labels
- [ ] Uses shadcn/ui components where applicable
- [ ] Uses `cn()` for conditional classes
- [ ] Uses `@/` import aliases

## Reference

- Design Standards: https://design.strongtie.io/docs/design-standards
- Code Examples: https://design.strongtie.io/docs/design-standards-examples
- AGENTS.md Template: https://design.strongtie.io/docs/agents-template
