Skip to main content

schema.Query

Query provides programmatic access to the Reactive Data Client cache while maintaining the same high performance and referential equality guarantees expected of Reactive Data Client.

Query can be rendered using schema lookup hook useQuery()

Query members

schema

Schema used to retrieve/denormalize data from the Reactive Data Client cache. This accepts any Queryable schema: Entity, All, Collection, Query, and Union.

process(entries, ...args)

Takes the (denormalized) response as entries and arguments and returns the new response for use with useQuery

Usage

Maintaining sort after creates

Here we have an API that sorts based on the orderBy field. By wrapping our Collection in a Query that sorts, we can ensure we maintain the correct order after pushing new posts.

Our example code starts sorting by title. Try adding some posts and see them inserted in the correct sort order.

import { Entity, RestEndpoint } from '@data-client/rest';

class Post extends Entity {
  id = '';
  title = '';
  group = '';
  author = '';

  pk() {
    return this.id;
  }
}
export const getPosts = new RestEndpoint({
  path: '/:group/posts',
  searchParams: {} as { orderBy?: string; author?: string },
  schema: new schema.Query(
    new schema.Collection([Post], {
      nonFilterArgumentKeys: /orderBy/,
    }),
    (posts, { orderBy } = {}) => {
      if (orderBy) {
        return [...posts].sort((a, b) =>
          a[orderBy].localeCompare(b[orderBy]),
        );
      }
      return posts;
    },
  ),
});
import { useLoading } from '@data-client/hooks';
import { getPosts } from './getPosts';

export default function NewPost({ user }: { user: string }) {
  const ctrl = useController();

const [handlePress, loading] = useLoading(async e => {
    if (e.key === 'Enter') {
      const title = e.currentTarget.value;
      e.currentTarget.value = '';
      await ctrl.fetch(getPosts.push, {group: 'react'}, {
        title,
        author: user,
      });
    }
  });

  return (
    <div>
      <input type="text" onKeyDown={handlePress} />{loading ? ' ...' : ''}
    </div>
  );
}
import { useSuspense } from '@data-client/react';
import { getPosts } from './getPosts';
import NewPost from './NewPost';

export default function PostList({
  user,
}) {
  const posts = useSuspense(getPosts, { author: user, orderBy: 'title', group: 'react' });
  return (
    <div>
      {posts.map(post => (
        <div key={post.pk()}>{post.title}</div>
      ))}
      <NewPost user={user} />
    </div>
  );
}
import PostList from './PostList';

function UserList() {
  const users = ['bob', 'clara']
  return (
    <div>
      {users.map(user => (
        <section key={user}>
          <h3>{user}</h3>
          <PostList user={user} />
        </section>
      ))}
    </div>
  );
}
render(<UserList />);
🔴 Live Preview
Store

Aggregates

Fixtures
GET /users
[{"id":"123","name":"Jim"},{"id":"456","name":"Jane"},{"id":"777","name":"Albatras","isAdmin":true}]
api/User
export class User extends Entity {
  id = '';
  name = '';
  isAdmin = false;
  pk() {
    return this.id;
  }
}
export const UserResource = createResource({
  path: '/users/:id',
  schema: User,
});
UsersPage
import { schema } from '@data-client/rest';
import { useQuery, useFetch } from '@data-client/react';
import { UserResource, User } from './api/User';

const getUserCount = new schema.Query(
  new schema.All(User),
  (entries, { isAdmin } = {}) => {
    if (isAdmin !== undefined)
      return entries.filter(user => user.isAdmin === isAdmin).length;
    return entries.length;
  },
);

function UsersPage() {
  useFetch(UserResource.getList);
  const userCount = useQuery(getUserCount);
  const adminCount = useQuery(getUserCount, { isAdmin: true });
  if (userCount === undefined) return <div>No users in cache yet</div>;
  return (
    <div>
      <div>Total users: {userCount}</div>
      <div>Total admins: {adminCount}</div>
    </div>
  );
}
render(<UsersPage />);
🔴 Live Preview
Store

Client side joins

Even if the network responses don't nest data, we can perform client-side joins by specifying the relationship in Entity.schema

api/User.ts
export class User extends Entity {
  id = 0;
  name = '';
  email = '';
  website = '';
  pk() {
    return `${this.id}`;
  }
}
export const UserResource = createResource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/users/:id',
  schema: User,
});
api/Todo.ts
import { User } from './User';

export class Todo extends Entity {
  id = 0;
  userId = User.fromJS({});
  title = '';
  completed = false;
  pk() {
    return `${this.id}`;
  }
  static schema = {
    userId: User,
  };
}
export const TodoResource = createResource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
});
TodoJoined.tsx
import { schema } from '@data-client/rest';
import { useQuery, useFetch } from '@data-client/react';
import { TodoResource, Todo } from './api/Todo';
import { UserResource, User } from './api/User';

const todosWithUser = new schema.Query(
  new schema.All(Todo),
  (entries, { userId = 0 }) => {
    return entries.filter(todo => todo.userId?.id === userId);
  },
);

function TodosPage() {
  useFetch(UserResource.getList);
  useFetch(TodoResource.getList);
  const todos = useQuery(todosWithUser, { userId: 1 });
  if (!todos) return <div>No Todos in cache yet</div>;
  if (!todos.length) return <div>No Todos match user</div>;
  return (
    <div>
      {todos.map(todo => (
        <div key={todo.pk()}>
          {todo.title} by {todo.userId.name}
        </div>
      ))}
    </div>
  );
}
render(<TodosPage />);
🔴 Live Preview
Store

Rearranging data with groupBy aggregations

api/User.ts
export class User extends Entity {
  id = 0;
  name = '';
  email = '';
  website = '';
  pk() {
    return `${this.id}`;
  }
}
export const UserResource = createResource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/users/:id',
  schema: User,
});
api/Todo.ts
import { User } from './User';

export class Todo extends Entity {
  id = 0;
  userId = User.fromJS({});
  title = '';
  completed = false;
  pk() {
    return `${this.id}`;
  }
  static schema = {
    userId: User,
  };
}
export const TodoResource = createResource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
  searchParams: {} as { userId?: string | number } | undefined,
});
TodoJoined.tsx
import { schema } from '@data-client/rest';
import { useQuery, useFetch } from '@data-client/react';
import { TodoResource, Todo } from './api/Todo';
import { UserResource } from './api/User';

const groupTodoByUser = new schema.Query(
  TodoResource.getList.schema,
  todos => {
    return Object.groupBy(todos, todo => todo?.userId?.username) as Record<
      string,
      Todo[]
    >;
  },
);

function TodosPage() {
  useFetch(UserResource.getList);
  useSuspense(TodoResource.getList);
  useSuspense(UserResource.getList);
  const todoByUser = useQuery(groupTodoByUser);
  if (!todoByUser) return <div>Todos not found</div>;
  return (
    <div>
      {Object.keys(todoByUser).map(username => (
        <div key={username}>
          <h3>
            {username} has {tasksRemaining(todoByUser[username])} tasks
            left
          </h3>
          {todoByUser[username].slice(0, 3).map(todo => (
            <div key={todo.pk()}>
              {todo.title} by {todo?.userId?.name}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

function tasksRemaining(todos: Todo[]) {
  return todos.filter(({ completed }) => !completed).length;
}
render(<TodosPage />);
🔴 Live Preview
Store