Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

4.5 KiB

paths
paths
apps/sim/**/use-*.ts
apps/sim/**/hooks/**/*.ts

Hook Patterns

Structure

For server data, use a React Query hook from hooks/queries/ — do NOT useState + fetch here (see .claude/rules/sim-queries.md). This pattern is for UI/orchestration hooks that hold UI-only state and wrap callbacks.

interface UseFeatureProps {
  id: string
  onSelect?: (item: Item) => void
}

export function useFeature({ id, onSelect }: UseFeatureProps) {
  // 1. Refs for stable dependencies
  const idRef = useRef(id)
  const onSelectRef = useRef(onSelect)

  // 2. UI-only state (never server data)
  const [isOpen, setIsOpen] = useState(false)

  // 3. Sync refs
  useEffect(() => {
    idRef.current = id
    onSelectRef.current = onSelect
  }, [id, onSelect])

  // 4. Operations (useCallback with empty deps when using refs)
  const select = useCallback((item: Item) => {
    onSelectRef.current?.(item)
    setIsOpen(false)
  }, [])

  return { isOpen, setIsOpen, select }
}

Rules

  1. Single responsibility per hook
  2. Props interface required
  3. Refs for stable callback dependencies
  4. Wrap returned functions in useCallback
  5. Server data goes through React Query (hooks/queries/), never useState + fetch
  6. Keep only UI/orchestration state in these hooks

State shape

Never mirror a prop into state with useState(prop) + a syncing useEffect — a prop change clobbers in-progress local edits. Use the prop directly, reset via a remount key, or — when you must seed local state from a prop only on a transition (e.g. a modal opening) — adjust it during render with a prev-value tracker held in useState:

const [prevOpen, setPrevOpen] = useState(open)
if (prevOpen !== open) {
  setPrevOpen(open)
  if (open) setName(initialName) // closed → open only
}

React re-renders immediately with the corrected state without committing the stale value. Rules: the if (prev !== current) guard is mandatory (an unconditional setState in render loops forever), the tracker is set inside the guard, and you may only set the currently-rendering component's state this way. Hold the tracker in useState, not a useRef — React forbids reading/writing ref.current during render (react.dev, useRef → "Do not write or read ref.current during rendering"; the react-hooks refs lint flags it), and a useState tracker is concurrent-safe where a mutated ref is not (a discarded render rolls state back, not a ref).

The tracker's initial value decides mount behavior — choose it deliberately. The example seeds useState(open) because the modal mounts closed, so the first render's guard is false and nothing resets on mount (correct — name is already at its initial value). When the effect you're replacing did real work on mount — opening a panel because a prop already matches, seeding editable state from an already-present value, or a component that can mount in the active state — seed a sentinel the live value can't equal (e.g. useState<T | null>(null)), otherwise the guard is false on the first render and that mount action is silently dropped. Place the block before any early return.

Some existing components use a useRef prev-tracker (if (prevRef.current !== x) { prevRef.current = x; … }). It works but reads/writes a ref during render — prefer the useState form above for new code.

Only convert a useState to a useRef when the value is never read during render/JSX and is never a hook dependency — a value in a useEffect/useMemo/useCallback dep array must re-run the hook on change, so it stays state (see also rerender-state-only-in-handlers in sim-components.md). Convert only set-only values read solely inside handlers or effect bodies (e.g. a prompt-history index, a pending-upload URL). If a ref feeds render, mutating it won't re-render and the UI goes stale.

Model mutually-exclusive flags as ONE status enum, not several contradictory booleans. isLoading/isVerified/isInvalidOtp describing one machine collapse to status: 'idle' | 'verifying' | 'verified' | 'error' (+ errorMessage); derive any boolean a consumer still needs (status === 'error').

Derive busy/success from the mutation object — never duplicate mutation.isPending/mutation.isSuccess into local useState. Read them directly (mutation.isSuccess) and reset with mutation.reset(). A distinct phase the mutation doesn't cover — e.g. a pre-submit captcha/Turnstile gate that runs before mutate() — is not a duplicate; keep that flag.