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
139 lines
5.8 KiB
Plaintext
139 lines
5.8 KiB
Plaintext
---
|
|
description: React Query patterns for the Sim application
|
|
globs: ["apps/sim/hooks/queries/**/*.ts"]
|
|
---
|
|
# React Query Patterns
|
|
|
|
All React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations.
|
|
|
|
## Query Key Factory
|
|
|
|
Every query file defines a hierarchical keys factory with an `all` root key and intermediate plural keys for prefix-level invalidation:
|
|
|
|
```typescript
|
|
export const entityKeys = {
|
|
all: ['entity'] as const,
|
|
lists: () => [...entityKeys.all, 'list'] as const,
|
|
list: (workspaceId?: string) => [...entityKeys.lists(), workspaceId ?? ''] as const,
|
|
details: () => [...entityKeys.all, 'detail'] as const,
|
|
detail: (id?: string) => [...entityKeys.details(), id ?? ''] as const,
|
|
}
|
|
```
|
|
|
|
Never use inline query keys — always use the factory.
|
|
|
|
## File Structure
|
|
|
|
```typescript
|
|
// 1. Query keys factory
|
|
// 2. Types (if needed)
|
|
// 3. Private fetch functions (accept signal parameter)
|
|
// 4. Exported hooks
|
|
```
|
|
|
|
## Query Hook
|
|
|
|
- Every `queryFn` must destructure and forward `signal` for request cancellation
|
|
- Every query must have an explicit `staleTime`
|
|
- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys
|
|
- Same-origin JSON calls must go through `requestJson(contract, ...)` from `@/lib/api/client/request` against the contract in `@/lib/api/contracts/**`
|
|
|
|
```typescript
|
|
import { requestJson } from '@/lib/api/client/request'
|
|
import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'
|
|
|
|
async function fetchEntities(workspaceId: string, signal?: AbortSignal): Promise<EntityList> {
|
|
const data = await requestJson(listEntitiesContract, {
|
|
query: { workspaceId },
|
|
signal,
|
|
})
|
|
return data.entities
|
|
}
|
|
|
|
export function useEntityList(workspaceId?: string, options?: { enabled?: boolean }) {
|
|
return useQuery({
|
|
queryKey: entityKeys.list(workspaceId),
|
|
queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
|
|
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
|
staleTime: 60 * 1000,
|
|
placeholderData: keepPreviousData, // OK: workspaceId varies
|
|
})
|
|
}
|
|
```
|
|
|
|
## Mutation Hook
|
|
|
|
- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible
|
|
- Invalidation must cover all affected query key prefixes (lists, details, related views)
|
|
- Use `onSuccess` invalidation for plain mutations; use `onSettled` for optimistic mutations so the cache is reconciled on both success and error (see Optimistic Updates below)
|
|
- `mutationFn` calls go through `requestJson(contract, { body, signal })` from `@/lib/api/client/request` — same boundary rule as queries
|
|
|
|
```typescript
|
|
export function useCreateEntity() {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (body: CreateEntityBody) => requestJson(createEntityContract, { body }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
## Optimistic Updates
|
|
|
|
For optimistic mutations, use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error, ensuring the cache is always reconciled with the server.
|
|
|
|
```typescript
|
|
export function useUpdateEntity() {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: async (variables) => { /* ... */ },
|
|
onMutate: async (variables) => {
|
|
await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) })
|
|
const previous = queryClient.getQueryData(entityKeys.detail(variables.id))
|
|
queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic value */)
|
|
return { previous }
|
|
},
|
|
onError: (_err, variables, context) => {
|
|
queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous)
|
|
},
|
|
onSettled: (_data, _error, variables) => {
|
|
queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
|
|
queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) })
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
For optimistic mutations syncing with Zustand, use `createOptimisticMutationHandlers` from `@/hooks/queries/utils/optimistic-mutation`.
|
|
|
|
## useCallback Dependencies
|
|
|
|
Never include mutation objects (e.g., `createEntity`) in `useCallback` dependency arrays — the mutation object is not referentially stable and changes on every state update. The `.mutate()` and `.mutateAsync()` functions are stable in TanStack Query v5.
|
|
|
|
```typescript
|
|
// ✗ Bad — causes unnecessary recreations
|
|
const handler = useCallback(() => {
|
|
createEntity.mutate(data)
|
|
}, [createEntity]) // unstable reference
|
|
|
|
// ✓ Good — omit from deps, mutate is stable
|
|
const handler = useCallback(() => {
|
|
createEntity.mutate(data)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [data])
|
|
```
|
|
|
|
## Boundary Types
|
|
|
|
- Hooks import named type aliases from `@/lib/api/contracts/**` (e.g., `import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'`). Never write `z.input<...>` / `z.output<...>` in hooks, and never `import { z } from 'zod'` in client code.
|
|
- Raw `fetch` is allowed only for documented exceptions — multipart uploads, binary downloads, streaming responses, signed-URL flows, OAuth redirects, external origins. Each such raw `fetch(` inside `apps/sim/hooks/queries/**` or `apps/sim/hooks/selectors/**` — and any same-origin `/api/...` fetch elsewhere under `apps/sim/**` outside an API route handler — must be preceded by a `// boundary-raw-fetch: <reason>` annotation (reason non-empty; up to three preceding comment lines tolerated). Enforced by `scripts/check-api-validation-contracts.ts` (`bun run check:api-validation` / `:strict`).
|
|
|
|
## Naming
|
|
|
|
- **Keys**: `entityKeys`
|
|
- **Query hooks**: `useEntity`, `useEntityList`
|
|
- **Mutation hooks**: `useCreateEntity`, `useUpdateEntity`, `useDeleteEntity`
|
|
- **Fetch functions**: `fetchEntity`, `fetchEntities` (private)
|