# View Data Hooks

> useViewQuery and useIndex for loading and joining data inside a view

Custom views load data through two hooks exported from `"struere/view"`: `useViewQuery` for live Convex subscriptions and `useIndex` for client-side joins. Both run inside the dashboard's `<ViewHost>` and respect the viewer's permissions.

## useViewQuery

`useViewQuery` resolves one of the view's declared queries to live data.

```tsx
import { useViewQuery } from "struere/view"

const sessions = useViewQuery<EntityRow>("sessions")
```

### Signature

```ts
function useViewQuery<T = unknown>(
  name: string,
  runtimeParams?: Record<string, unknown>
): { data: T[] | undefined; loading: boolean; error?: Error }
```

| Field | Description |
|-------|-------------|
| `name` | A key from the view's `queries` object |
| `runtimeParams` | Optional overrides merged into `filters`, `status`, and `limit` |
| `data` | Array of rows, or `undefined` while loading |
| `loading` | `true` while the initial subscription is in flight |
| `error` | Set if the server rejected the query (permissions, missing type, etc.) |

### Reactivity

`useViewQuery` wraps Convex's `useQuery` on `customViews.runViewQuery`. The dashboard keeps the subscription open as long as the view is mounted. When any entity in the result set is created, updated, or deleted, `data` is replaced — no refresh needed.

### Lifecycle

1. The view mounts. `loading` is `true`, `data` is `undefined`.
2. The server runs `assertCanPerform("list", entityType)` against the viewer's `ActorContext`.
3. Scope filters from the viewer's roles are applied.
4. Field masks are applied to each row.
5. Rows arrive. `loading` becomes `false`, `data` is the array.
6. Subsequent mutations in the same environment update `data` in place.

### Runtime Params

`runtimeParams` lets you narrow a declared query at render time. The CLI compiler can only see literal values for `queries`, so anything dynamic — a user-selected filter, a date range, a search box — goes through this argument.

```tsx
const sessions = useViewQuery("sessions", {
  filters: { teacherId: selectedTeacherId },
  limit: 50,
})
```

### Permission Propagation

Every `useViewQuery` call routes through `runViewQuery`, which builds an `ActorContext` from the signed-in user's roles and environment. The result is:

- Rows the viewer cannot `list` are excluded.
- Scope rules filter rows server-side (no client-side leakage).
- Field masks redact disallowed fields before the row leaves Convex.

A view never returns more than its viewer can see, even if the query declared `limit: 1000`.

## useIndex

`useIndex` builds a `Map` keyed by one field on each row — the standard pattern for client-side joins between two queries.

```tsx
import { useIndex, useViewQuery } from "struere/view"

const teachers = useViewQuery<Teacher>("teachers")
const teacherById = useIndex(teachers.data, "id")

teacherById.get(session.teacherId) // -> Teacher | undefined
```

### Signature

```ts
function useIndex<T, K extends keyof T>(
  rows: T[] | undefined,
  key: K
): Map<T[K], T>
```

The hook memoizes the map across renders. When `rows` is undefined, you get an empty `Map`. When `rows` is replaced (typical with live subscriptions), the map is rebuilt.

### When to Use

Reach for `useIndex` whenever the same lookup repeats inside a render — for example, mapping a `teacherId` on every row of a sessions table back to the teacher's name. The alternative — calling `teachers.find(...)` inside a column cell — is O(n*m) and re-evaluates on every keystroke in an upstream filter.

## Row Shape

Today, `useViewQuery` returns rows in their raw entity shape:

```ts
{ _id: string; data: Record<string, unknown> }
```

The generic `T` is a typing affordance, not a transform. Until a typed helper ships, flatten rows at the top of `render`:

```tsx
interface EntityRow {
  _id: string
  data: Record<string, unknown>
}

function flatten<T>(rows: EntityRow[] | undefined): T[] {
  if (!rows) return []
  return rows.map((row) => ({ id: row._id, ...(row.data ?? {}) })) as T[]
}

const sessionsQ = useViewQuery<EntityRow>("sessions")
const sessions = flatten<Session>(sessionsQ.data)
```

Type-safe row helpers are on the roadmap. Until they land, the `flatten` pattern keeps the rest of the view typed.

## Full Example

```tsx
import {
  defineView,
  Card,
  DataTable,
  EmptyState,
  Grid,
  KPI,
  Stack,
  useIndex,
  useViewQuery,
} from "struere/view"

interface EntityRow {
  _id: string
  data: Record<string, unknown>
}

interface Session {
  id: string
  status: string
  teacherId: string
  subject: string
}

interface Teacher {
  id: string
  name: string
}

function flatten<T>(rows: EntityRow[] | undefined): T[] {
  if (!rows) return []
  return rows.map((row) => ({ id: row._id, ...(row.data ?? {}) })) as T[]
}

export default defineView({
  name: "Sessions by Teacher",
  slug: "sessions-by-teacher",
  queries: {
    sessions: { type: "session", limit: 100 },
    teachers: { type: "teacher", limit: 100 },
  },
  render: () => {
    const sessionsQ = useViewQuery<EntityRow>("sessions")
    const teachersQ = useViewQuery<EntityRow>("teachers")

    if (sessionsQ.loading || teachersQ.loading) {
      return <EmptyState title="Loading..." />
    }

    const sessions = flatten<Session>(sessionsQ.data)
    const teachers = flatten<Teacher>(teachersQ.data)
    const teacherById = useIndex(teachers, "id")

    return (
      <Stack gap="md">
        <Grid columns={{ sm: 1, md: 2 }} gap="md">
          <KPI label="Sessions" value={sessions.length} />
          <KPI label="Teachers" value={teachers.length} />
        </Grid>
        <Card title="Sessions">
          <DataTable<Session>
            rows={sessions}
            rowKey={(row) => row.id}
            columns={[
              { key: "subject", header: "Subject", cell: (row) => row.subject },
              {
                key: "teacher",
                header: "Teacher",
                cell: (row) => teacherById.get(row.teacherId)?.name ?? row.teacherId,
              },
              { key: "status", header: "Status", cell: (row) => row.status },
            ]}
          />
        </Card>
      </Stack>
    )
  },
})
```
