Skip to main content

useQuery()

Data rendering without the fetch.

Access any Queryable Schema's store value; like Entity, All, Collection, Query, Union, and Scalar. Lazy fields also work via their .query accessor. If the value does not exist, returns undefined.

useQuery() is reactive to data mutations; rerendering only when necessary. Returns undefined when data is Invalid.

tip

Queries are a great companion to efficiently render aggregate computations like those that use groupBy, map, reduce, and filter.

Usage​

<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>

See truthiness narrowing for more information about type handling

Types​

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

Arguments can be plain values, refs (including 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 schemas require an queryKey() method that returns something. These include Entity, All, Collection, Query, Union, and Scalar. Lazy fields produce a Queryable via their .query accessor.

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

Examples​

Sorting & Filtering​

Query sorts and filters what is already in the store, so picking another column re-sorts the table without fetching again.

<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>

Lazy relationships​

Lazy fields keep raw IDs during parent denormalization. Use .query with useQuery to resolve them on demand, isolating re-renders to only the components that need the related data.

<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>
<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>