useQuery() - Normalized data store access in React

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.

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

See truthiness narrowing for more information about type handling

  • Type
  • With Generics
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
): {};
}

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 />);

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 />);

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