Skip to main content

useQuery()

Data rendering without the fetch.

Access any Queryable Schema's store value; like Entity, All, Collection, Query, and Union. 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

import { Query } from '@data-client/rest';
import { useQuery } from '@data-client/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 });
  return (
    <center>
      <small>{totalVotes} votes total</small>
    </center>
  );
}
interface Props {
  userId: number;
}
🔴 Live Preview
Store

See truthiness narrowing for more information about type handling

Types

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

Queryable

Queryable schemas require an queryKey() method that returns something. These include Entity, All, Collection, Query, and Union. 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 provides programmatic access to the Reactive Data Client store.

import { Query } from '@data-client/rest';
import { useQuery, useFetch } from '@data-client/react';
import { UserResource, User } from './UserResource';

interface Args {
  asc: boolean;
  isAdmin?: boolean;
}
const sortedUsers = new Query(
  new All(User),
  (entries, { asc, isAdmin }: Args = { asc: false }) => {
    let sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
    if (isAdmin !== undefined)
      sorted = sorted.filter(user => user.isAdmin === isAdmin);
    if (asc) return sorted;
    return sorted.reverse();
  },
);

function UsersPage() {
  useFetch(UserResource.getList);
  const users = useQuery(sortedUsers, { asc: true });
  if (!users) return <div>No users in cache yet</div>;
  return (
    <div>
      {users.map(user => (
        <div key={user.pk()}>{user.name}</div>
      ))}
    </div>
  );
}
render(<UsersPage />);
🔴 Live Preview
Store

Remaining Todo total

Queries can also be used to compute aggregates

More Demos

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.

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 <div>Loading...</div>;
  return (
    <div>
      {departments.map(dept => (
        <div key={dept.pk()}>
          <strong>{dept.name}</strong>: <BuildingList dept={dept} />
        </div>
      ))}
    </div>
  );
}
render(<DepartmentsPage />);
🔴 Live Preview
Store

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.

More Demos