# 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/docs/getting-started/mutations.md); rerendering only when necessary. Returns `undefined`
when data is [Invalid](https://dataclient.io/docs/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,
    };
  },
});
```

```tsx title="PostItem" {8}
import { useController } from '@data-client/react';
import NumberFlow from '@number-flow/react';
import { PostResource, type Post } from './PostResource';

export default function PostItem({ post }: Props) {
  const ctrl = useController();
  const handleVote = () => {
    ctrl.fetch(PostResource.vote, { id: post.id });
  };
  return (
    <article className="listItem">
      <button
        className="upvote"
        onClick={handleVote}
        aria-label={`Upvote, ${post.votes} votes`}
      >
        <NumberFlow value={post.votes} />
      </button>
      <div>
        <h4>{post.title}</h4>
        <p className="meta">{post.body}</p>
      </div>
    </article>
  );
}
interface Props {
  post: Post;
}
```

```tsx title="TotalVotes" {12}
import { Query } from '@data-client/rest';
import { useQuery } from '@data-client/react';
import NumberFlow from '@number-flow/react';
import { PostResource } from './PostResource';

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

export default function TotalVotes({ userId }: Props) {
  const totalVotes = useQuery(queryTotalVotes, { userId });
  if (totalVotes === undefined) return null;
  return (
    <span className="meta">
      <NumberFlow value={totalVotes} /> votes
    </span>
  );
}
interface Props {
  userId: number;
}
```

```tsx title="PostList"
import { useSuspense } from '@data-client/react';
import { PostResource } from './PostResource';
import PostItem from './PostItem';
import TotalVotes from './TotalVotes';

function PostList() {
  const userId = 2;
  const posts = useSuspense(PostResource.getList, { userId });
  return (
    <>
      <header className="appHeader">
        <h1 className="appTitle">Posts</h1>
        <TotalVotes userId={userId} />
      </header>
      <div className="list">
        {posts.map(post => (
          <PostItem key={post.pk()} post={post} />
        ))}
      </div>
    </>
  );
}
render(<PostList />);
```

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

## Types

```typescript
function useQuery(
  schema: Queryable,
  ...args: SchemaArgs<typeof schema>
): DenormalizeNullable<typeof endpoint.schema> | undefined;
```

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

### 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;
}
```

```tsx title="RoleFilter"
const roles = { All: undefined, Admins: true, Members: false };

export default function RoleFilter({ isAdmin, onChange }: Props) {
  return (
    <div className="segmented" role="group" aria-label="Role">
      {Object.entries(roles).map(([label, value]) => (
        <button
          key={label}
          aria-pressed={isAdmin === value}
          onClick={() => onChange(value)}
        >
          {label}
        </button>
      ))}
    </div>
  );
}
interface Props {
  isAdmin: boolean | undefined;
  onChange: (isAdmin: boolean | undefined) => void;
}
```

```tsx title="UsersTableView"
import React from 'react';
import { type Sort, type SortBy, type User } from './resources/User';

export default function UsersTableView({ users, sort, onSort }: Props) {
  return (
    <table>
      <thead>
        <tr>
          <SortHeader column="name" sort={sort} onSort={onSort}>
            Name
          </SortHeader>
          <th>Role</th>
          <SortHeader column="posts" sort={sort} onSort={onSort} numeric>
            Posts
          </SortHeader>
          <SortHeader column="joined" sort={sort} onSort={onSort} numeric>
            Joined
          </SortHeader>
        </tr>
      </thead>
      <tbody>
        {users.map(user => (
          <tr key={user.pk()}>
            <td>
              <Avatar name={user.name} />
              {user.name}
            </td>
            <td>
              <span className={user.isAdmin ? 'pill primary' : 'pill'}>
                {user.isAdmin ? 'Admin' : 'Member'}
              </span>
            </td>
            <td className="value">{user.posts}</td>
            <td className="value">{user.joined}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
interface Props {
  users: User[];
  sort: Sort;
  onSort: (sort: Sort) => void;
}

/** A column's header: sorts by it, and a second click reverses it */
function SortHeader({ column, sort, onSort, numeric, children }: HeaderProps) {
  const sorted = sort.sortBy === column;
  return (
    <th
      className={numeric ? 'value' : undefined}
      aria-sort={
        sorted ? (sort.desc ? 'descending' : 'ascending') : undefined
      }
    >
      <button
        className="sort"
        onClick={() => onSort({ sortBy: column, desc: sorted && !sort.desc })}
      >
        {children}
      </button>
    </th>
  );
}
interface HeaderProps {
  column: SortBy;
  sort: Sort;
  onSort: (sort: Sort) => void;
  numeric?: boolean;
  children: React.ReactNode;
}
```

```tsx title="UsersTable" {27}
import { All, Query } from '@data-client/rest';
import { useQuery, useSuspense } from '@data-client/react';
import { type Sort, User, UserResource } from './resources/User';
import UsersTableView from './UsersTableView';

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;
  },
);

export default function UsersTable({ sort, isAdmin, onSort }: Props) {
  useSuspense(UserResource.getList);
  const users = useQuery(sortedUsers, { ...sort, isAdmin }) ?? [];
  return <UsersTableView users={users} sort={sort} onSort={onSort} />;
}
interface Props {
  sort: Sort;
  isAdmin: boolean | undefined;
  onSort: (sort: Sort) => void;
}
```

```tsx title="UsersPage"
import React from 'react';
import RoleFilter from './RoleFilter';
import { type Sort } from './resources/User';
import UsersTable from './UsersTable';

function UsersPage() {
  const [sort, setSort] = React.useState<Sort>({
    sortBy: 'name',
    desc: false,
  });
  const [isAdmin, setIsAdmin] = React.useState<boolean>();
  return (
    <>
      <header className="appHeader">
        <h1 className="appTitle">Team</h1>
        <RoleFilter isAdmin={isAdmin} onChange={setIsAdmin} />
      </header>
      <UsersTable sort={sort} isAdmin={isAdmin} onSort={setSort} />
    </>
  );
}
render(<UsersPage />);
```

### Remaining Todo total

[Queries](https://dataclient.io/rest/api/Query.md) can also be used to compute aggregates

Example app: [todo-app](https://github.com/reactive/data-client/tree/master/examples/todo-app) ([`src/resources/TodoResource.ts`](https://github.com/reactive/data-client/blob/master/examples/todo-app/src/resources/TodoResource.ts), [`src/pages/Home/TodoStats.tsx`](https://github.com/reactive/data-client/blob/master/examples/todo-app/src/pages/Home/TodoStats.tsx))

### 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,
});
```

```tsx title="DepartmentsPage" {6-9}
import { All } from '@data-client/rest';
import { useQuery, useFetch } from '@data-client/react';
import { DepartmentResource, Department } from './Resources';

function BuildingList({ dept }: { dept: Department }) {
  const buildings = useQuery(
    Department.schema.buildings.query,
    dept.buildings,
  );
  if (!buildings) return null;
  return <span>{buildings.map(b => b.name).join(', ')}</span>;
}

function DepartmentsPage() {
  useFetch(DepartmentResource.getList);
  const departments = useQuery(new All(Department));
  if (!departments) return <Loading />;
  return (
    <div>
      {departments.map(dept => (
        <div key={dept.pk()}>
          <strong>{dept.name}</strong>: <BuildingList dept={dept} />
        </div>
      ))}
    </div>
  );
}
render(<DepartmentsPage />);
```

### Data fallbacks

In this case `Ticker` is constantly updated from a websocket stream. However, there is no bulk/list
fetch for `Ticker` - making it inefficient for getting the prices on a list view.

So in this case we can fetch a list of `Stats` as a fallback since it has price data as well.

Example app: [coin-app](https://github.com/reactive/data-client/tree/master/examples/coin-app) ([`src/pages/Home/CurrencyList.tsx`](https://github.com/reactive/data-client/blob/master/examples/coin-app/src/pages/Home/CurrencyList.tsx), [`src/resources/fallbackQueries.ts`](https://github.com/reactive/data-client/blob/master/examples/coin-app/src/resources/fallbackQueries.ts), [`src/pages/Home/AssetPrice.tsx`](https://github.com/reactive/data-client/blob/master/examples/coin-app/src/pages/Home/AssetPrice.tsx))
