Navigation: Root AGENTS.md → Web → Hooks
- Always prefix with
use. - Use descriptive names matching the domain:
useWorkflowActionsnotuseActions. - Include TypeScript types for all parameters and return values.
- Return objects (not arrays) for multiple return values.
- Place tests in
__tests__/subdirectories.
- Use for: side effects (network, subscriptions, timers, DOM mutations).
- Don't use for: deriving data from props/state — compute it during render instead.
- Every value used inside must be in the dependency array.
// ❌ Bad — should be: const value = a + b
useEffect(() => setValue(a + b), [a, b]);- Use for: expensive computation, referential stability for child props or dependency arrays.
- Don't use for: cheap computation.
// ❌ Bad — pointless memoization
const sum = useMemo(() => a + b, [a, b]);- Use for: passing functions to memoized children, function is a dependency of useEffect/useMemo.
- Don't use for: functions used only locally, children that aren't memoized.
- Use for: pure component, stable props, renders often, expensive rendering.
- Don't use for: props that change every render, small/cheap components.
Never add these "just in case." If performance is fine, do nothing.
// ✅ Good — selective subscription
const selectedNodes = useNodeStore(state => state.nodes.filter(n => n.selected));
// ❌ Bad — subscribes to entire store
const store = useNodeStore();Hooks that launch and track workflow runs (sketch/useGenerateLayer,
timeline/useGenerateClip, useRegenerateStaleLayers, miniapp runners, node
exec-state hooks) must assume multiple runs of the same workflow execute at
once. Lessons from shipped fixes:
- Resolve a job's output/error from that job's own messages, not a shared
store. Capture each job's
output_updatevalue and node errors into per-job maps keyed byjobIdfrom the live stream; on completion readextractAssetId(jobOutputs.get(jobId)). Resolving viaresolveOutputAssetId(workflowId, nodeId)against the sharedResultsStorelets concurrent runs read each other's results. - Use
jobIdreturned fromrun()— don't readrunnerStore.job_idafter starting a run (it may point at a different, still-active job). - Guard shared-slot resets on
store.job_id === jobIdbefore clearing per-workflow runner state from a terminaljob_update. - Per-node status is the source of truth for "running now", not the coarse
run-level
RunState— run-level state lags per-node updates (a run can execute nodes while stillqueued). Use run-level state only as a negative filter (skipTERMINAL_RUN_STATES), asuseNodeActiveRunCountdoes. - Regression test with two concurrent same-workflow jobs driven through the
real handler (see
*.concurrency.test.ts,ambientLiveness.repro.test.ts).
cd web
npm test -- --testPathPattern=hooks # Hook tests only- Use
renderHookfrom@testing-library/react. - Use
actfor state updates. - Mock stores and external dependencies.