# useQuery()

Data rendering without the fetch.

Access any [Queryable Schema](https://dataclient.io/rest/api/schema.md#queryable)'s store value; like [Entity](https://dataclient.io/rest/api/Entity.md), [All](https://dataclient.io/rest/api/All.md), [Collection](https://dataclient.io/rest/api/Collection.md), [Query](https://dataclient.io/rest/api/Query.md),
[Union](https://dataclient.io/rest/api/Union.md), and [Scalar](https://dataclient.io/rest/api/Scalar.md). [Lazy](https://dataclient.io/rest/api/Lazy.md) fields also work via their [`.query`](https://dataclient.io/rest/api/Lazy.md#query) accessor.
If the value does not exist, returns `undefined`.

`useQuery()` is reactive to data [mutations](https://dataclient.io/vue/getting-started/mutations.md); rerendering only when necessary. Returns `undefined`
when data is [Invalid](https://dataclient.io/vue/concepts/expiry-policy.md#invalid).

> **Tip**
>
> [Queries](https://dataclient.io/rest/api/Query.md) are a great companion to efficiently render aggregate computations like those that use [groupBy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy#browser_compatibility),
> [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map), [reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce), and [filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).

## Usage

```ts title="Post"
import { Entity, EntityMixin } from '@data-client/rest';

export class Post extends Entity {
  id = 0;
  author = { id: 0 };
  title = '';
  body = '';
  votes = 0;

  static key = 'Post';

  static schema = {
    author: EntityMixin(
      class User {
        id = 0;
      },
    ),
  };
}
```

```ts title="PostResource" {15-22}
import { resource } from '@data-client/rest';
import { Post } from './Post';

export { Post };

export const PostResource = resource({
  path: '/posts/:id',
  searchParams: {} as { userId?: string | number } | undefined,
  schema: Post,
}).extend('vote', {
  path: '/posts/:id/vote',
  method: 'POST',
  body: undefined,
  schema: Post,
  getOptimisticResponse(snapshot, { id }) {
    const post = snapshot.get(Post, { id });
    if (!post) throw snapshot.abort;
    return {
      id,
      votes: post.votes + 1,
    };
  },
});
```

```html title="PostItem.vue" {10}
<script setup lang="ts">
  import { useController } from '@data-client/vue';
  import NumberFlow from '@number-flow/vue';
  import { PostResource, type Post } from './PostResource';

  const props = defineProps<{ post: Post }>();
  const ctrl = useController();

  const handleVote = () => {
    ctrl.fetch(PostResource.vote, { id: props.post.id });
  };
</script>

<template>
  <article class="listItem">
    <button
      class="upvote"
      @click="handleVote"
      :aria-label="`Upvote, ${post.votes} votes`"
    >
      <NumberFlow :value="post.votes" />
    </button>
    <div>
      <h4>{{ post.title }}</h4>
      <p class="meta">{{ post.body }}</p>
    </div>
  </article>
</template>
```

```html title="TotalVotes.vue" {13}
<script setup lang="ts">
  import { Query } from '@data-client/rest';
  import { useQuery } from '@data-client/vue';
  import NumberFlow from '@number-flow/vue';
  import { PostResource } from './PostResource';

  const queryTotalVotes = new Query(
    PostResource.getList.schema,
    posts => posts.reduce((total, post) => total + post.votes, 0),
  );

  const props = defineProps<{ userId: number }>();
  const totalVotes = useQuery(queryTotalVotes, () => ({ userId: props.userId }));
</script>

<template>
  <span v-if="totalVotes !== undefined" class="meta">
    <NumberFlow :value="totalVotes" /> votes
  </span>
</template>
```

```html title="PostList.vue"
<script setup lang="ts">
  import { useSuspense } from '@data-client/vue';
  import { PostResource } from './PostResource';
  import PostItem from './PostItem.vue';
  import TotalVotes from './TotalVotes.vue';

  const userId = 2;
  const posts = await useSuspense(PostResource.getList, { userId });
</script>

<template>
  <header class="appHeader">
    <h1 class="appTitle">Posts</h1>
    <TotalVotes :userId="userId" />
  </header>
  <div class="list">
    <PostItem v-for="post in posts" :key="post.pk()" :post="post" />
  </div>
</template>
```

See [truthiness narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing) for
more information about type handling

## Types

```typescript
function useQuery<S extends Queryable>(
  schema: S,
  ...args: MaybeRefsOrGetters<SchemaArgs<S>>
): ComputedRef<DenormalizeNullable<S> | undefined>;
```

Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter
functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't
follow prop or route changes, so use a getter or `computed` when an argument can change.

The result updates when the arguments change.

### Queryable

[Queryable](https://dataclient.io/rest/api/schema.md#queryable) schemas require an `queryKey()` method that returns something. These include
[Entity](https://dataclient.io/rest/api/Entity.md), [All](https://dataclient.io/rest/api/All.md), [Collection](https://dataclient.io/rest/api/Collection.md), [Query](https://dataclient.io/rest/api/Query.md),
[Union](https://dataclient.io/rest/api/Union.md), and [Scalar](https://dataclient.io/rest/api/Scalar.md). [Lazy](https://dataclient.io/rest/api/Lazy.md) fields produce a Queryable via their [`.query`](https://dataclient.io/rest/api/Lazy.md#query) accessor.

```ts
interface Queryable {
  queryKey(
    args: readonly any[],
    queryKey: (...args: any) => any,
    getEntity: GetEntity,
    getIndex: GetIndex,
    // Must be non-void
  ): {};
}
```

## Examples

### Sorting & Filtering

[Query](https://dataclient.io/rest/api/Query.md) sorts and filters what is already in the store, so picking another column
re-sorts the table without fetching again.

```ts title="resources/User"
import { Entity, resource } from '@data-client/rest';

export class User extends Entity {
  id = '';
  name = '';
  isAdmin = false;
  posts = 0;
  joined = 0;

  static key = 'User';
}
export const UserResource = resource({
  path: '/users/:id',
  schema: User,
});

/** A users list's order: by one column, reversed when `desc` */
export type SortBy = 'name' | 'posts' | 'joined';
export interface Sort {
  sortBy: SortBy;
  desc: boolean;
}
```

```html title="RoleFilter.vue"
<script setup lang="ts">
  const roles = { All: undefined, Admins: true, Members: false };

  defineProps<{ isAdmin: boolean | undefined }>();
  const emit = defineEmits<{ change: [isAdmin: boolean | undefined] }>();
</script>

<template>
  <div class="segmented" role="group" aria-label="Role">
    <button
      v-for="(value, label) in roles"
      :key="label"
      :aria-pressed="isAdmin === value"
      @click="emit('change', value)"
    >
      {{ label }}
    </button>
  </div>
</template>
```

```html title="UsersTableView.vue"
<script setup lang="ts">
  import { type Sort, type SortBy, type User } from './resources/User';

  const props = defineProps<{ users: readonly User[]; sort: Sort }>();
  const emit = defineEmits<{ sort: [sort: Sort] }>();

  function ariaSort(column: SortBy) {
    if (props.sort.sortBy !== column) return undefined;
    return props.sort.desc ? 'descending' : 'ascending';
  }
  // a second click on the sorted column reverses it
  function toggleSort(column: SortBy) {
    const desc = props.sort.sortBy === column && !props.sort.desc;
    emit('sort', { sortBy: column, desc });
  }
</script>

<template>
  <table>
    <thead>
      <tr>
        <th :aria-sort="ariaSort('name')">
          <button class="sort" @click="toggleSort('name')">Name</button>
        </th>
        <th>Role</th>
        <th class="value" :aria-sort="ariaSort('posts')">
          <button class="sort" @click="toggleSort('posts')">Posts</button>
        </th>
        <th class="value" :aria-sort="ariaSort('joined')">
          <button class="sort" @click="toggleSort('joined')">Joined</button>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="user in users" :key="user.pk()">
        <td>
          <Avatar :name="user.name" />
          {{ user.name }}
        </td>
        <td>
          <span :class="user.isAdmin ? 'pill primary' : 'pill'">
            {{ user.isAdmin ? 'Admin' : 'Member' }}
          </span>
        </td>
        <td class="value">{{ user.posts }}</td>
        <td class="value">{{ user.joined }}</td>
      </tr>
    </tbody>
  </table>
</template>
```

```html title="UsersTable.vue" {34-37}
<script lang="ts">
  import { All, Query } from '@data-client/rest';
  import { type Sort, User } from './resources/User';

  interface Args extends Sort {
    /** admins only (true), members only (false), or everyone */
    isAdmin: boolean | undefined;
  }

  const sortedUsers = new Query(
    new All(User),
    (entries, { sortBy, desc, isAdmin }: Args) => {
      const users = entries
        .filter(user => isAdmin === undefined || user.isAdmin === isAdmin)
        .sort((a, b) =>
          sortBy === 'name' ?
            a.name.localeCompare(b.name)
          : a[sortBy] - b[sortBy],
        );
      return desc ? users.reverse() : users;
    },
  );
</script>

<script setup lang="ts">
  import { useQuery, useSuspense } from '@data-client/vue';
  import { UserResource } from './resources/User';
  import UsersTableView from './UsersTableView.vue';

  const props = defineProps<{ sort: Sort; isAdmin: boolean | undefined }>();
  defineEmits<{ sort: [sort: Sort] }>();

  await useSuspense(UserResource.getList);
  const users = useQuery(sortedUsers, () => ({
    ...props.sort,
    isAdmin: props.isAdmin,
  }));
</script>

<template>
  <UsersTableView :users="users ?? []" :sort="sort" @sort="$emit('sort', $event)" />
</template>
```

```html title="UsersPage.vue"
<script setup lang="ts">
  import { ref } from 'vue';
  import RoleFilter from './RoleFilter.vue';
  import { type Sort } from './resources/User';
  import UsersTable from './UsersTable.vue';

  const sort = ref<Sort>({ sortBy: 'name', desc: false });
  const isAdmin = ref<boolean>();
</script>

<template>
  <header class="appHeader">
    <h1 class="appTitle">Team</h1>
    <RoleFilter :is-admin="isAdmin" @change="isAdmin = $event" />
  </header>
  <UsersTable :sort="sort" :is-admin="isAdmin" @sort="sort = $event" />
</template>
```

### Lazy relationships

[Lazy](https://dataclient.io/rest/api/Lazy.md) fields keep raw IDs during parent denormalization. Use [`.query`](https://dataclient.io/rest/api/Lazy.md#query) with `useQuery` to resolve them on demand,
isolating re-renders to only the components that need the related data.

```ts title="Resources"
import { Entity, Lazy, resource } from '@data-client/rest';

export class Building extends Entity {
  id = '';
  name = '';

  static key = 'Building';
}

export class Department extends Entity {
  id = '';
  name = '';
  buildings: string[] = [];

  static schema = {
    buildings: new Lazy([Building]),
  };
  static key = 'Department';
}

export const DepartmentResource = resource({
  path: '/departments/:id',
  schema: Department,
});
```

```html title="BuildingList.vue" {8-11}
<script setup lang="ts">
  import { computed } from 'vue';
  import { useQuery } from '@data-client/vue';
  import { Department } from './Resources';

  const props = defineProps<{ dept: Department }>();

  const buildings = useQuery(
    Department.schema.buildings.query,
    computed(() => props.dept.buildings),
  );
</script>

<template>
  <span v-if="buildings">{{ buildings.map(b => b.name).join(', ') }}</span>
</template>
```

```html title="DepartmentsPage.vue"
<script setup lang="ts">
  import { All } from '@data-client/rest';
  import { useQuery, useFetch } from '@data-client/vue';
  import { DepartmentResource, Department } from './Resources';
  import BuildingList from './BuildingList.vue';

  useFetch(DepartmentResource.getList);
  const departments = useQuery(new All(Department));
</script>

<template>
  <Loading v-if="!departments" />
  <div v-else>
    <div v-for="dept in departments" :key="dept.pk()">
      <strong>{{ dept.name }}</strong>: <BuildingList :dept="dept" />
    </div>
  </div>
</template>
```
