Skip to content

Latest commit

 

History

History
373 lines (312 loc) · 11.2 KB

README.md

File metadata and controls

373 lines (312 loc) · 11.2 KB

Factory emoji

Query Key Factory

Latest build Latest published version Bundlephobia Tree shaking available Types included License Number of downloads GitHub Stars

Typesafe query key management for @tanstack/query with auto-completion features.

Focus on writing and invalidating queries without the hassle of remembering
how you've set up a key for a specific query! This lib will take care of the rest.

📦 Install

Query Key Factory is available as a package on NPM, install with your favorite package manager:

npm install @lukemorales/query-key-factory

⚡ Quick start

Start by defining the query keys for the features of your app:

Declare your store in a single file

import { createQueryKeyStore } from "@lukemorales/query-key-factory";

// if you prefer to declare everything in one file
export const queries = createQueryKeyStore({
  users: {
    all: null,
    detail: (userId: string) => ({
      queryKey: [userId],
      queryFn: () => api.getUser(userId),
    }),
  },
  todos: {
    detail: (todoId: string) => [todoId],
    list: (filters: TodoFilters) => ({
      queryKey: [{ filters }],
      queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
      contextQueries: {
        search: (query: string, limit = 15) => ({
          queryKey: [query, limit],
          queryFn: (ctx) => api.getSearchTodos({
            page: ctx.pageParam,
            filters,
            limit,
            query,
          }),
        }),
      },
    }),
  },
});

Fine-grained declaration colocated by features

import { createQueryKeys, mergeQueryKeys } from "@lukemorales/query-key-factory";

// queries/users.ts
export const users = createQueryKeys('users', {
  all: null,
  detail: (userId: string) => ({
    queryKey: [userId],
    queryFn: () => api.getUser(userId),
  }),
});

// queries/todos.ts
export const todos = createQueryKeys('todos', {
  detail: (todoId: string) => [todoId],
  list: (filters: TodoFilters) => ({
    queryKey: [{ filters }],
    queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
    contextQueries: {
      search: (query: string, limit = 15) => ({
        queryKey: [query, limit],
        queryFn: (ctx) => api.getSearchTodos({
          page: ctx.pageParam,
          filters,
          limit,
          query,
        }),
      }),
    },
  }),
});

// queries/index.ts
export const queries = mergeQueryKeys(users, todos);

Use throughout your codebase as the single source for writing the query keys, or even the complete queries for your cache management:

import { queries } from '../queries';

export function useUsers() {
  return useQuery({
    ...queries.users.all,
    queryFn: () => api.getUsers(),
  });
};

export function useUserDetail(id: string) {
  return useQuery(queries.users.detail(id));
};
import { queries } from '../queries';

export function useTodos(filters: TodoFilters) {
  return useQuery(queries.todos.list(filters));
};

export function useSearchTodos(filters: TodoFilters, query: string, limit = 15) {
  return useQuery({
    ...queries.todos.list(filters)._ctx.search(query, limit),
    enabled: Boolean(query),
  });
};

export function useUpdateTodo() {
  const queryClient = useQueryClient();

  return useMutation(updateTodo, {
    onSuccess(newTodo) {
      queryClient.setQueryData(queries.todos.detail(newTodo.id).queryKey, newTodo);

      // invalidate all the list queries
      queryClient.invalidateQueries({
        queryKey: queries.todos.list._def,
        refetchActive: false,
      });
    },
  });
};

📝 Features

Standardized keys

All keys generated follow the @tanstack/query convention of being an array at top level, including keys with serializable objects:

export const todos = createQueryKeys('todos', {
  detail: (todoId: string) => [todoId],
  list: (filters: TodoFilters) => ({
    queryKey: [{ filters }],
  }),
});

// => createQueryKeys output:
// {
//   _def: ['todos'],
//   detail: (todoId: string) => {
//     queryKey: ['todos', 'detail', todoId],
//   },
//   list: (filters: TodoFilters) => {
//     queryKey: ['todos', 'list', { filters }],
//   },
// }

queryKey can be optional when there's no need for a dynamic query:

export const users = createQueryKeys('users', {
  list: {
    queryKey: null,
    queryFn: () => api.getUsers(),
  }
});

Generate the query options you need to run useQuery

Declare your queryKey and your queryFn together, and have easy access to everything you need to run a query:

export const users = createQueryKeys('users', {
  detail: (userId: string) => ({
    queryKey: [userId],
    queryFn: () => api.getUser(userId),
  }),
});

// => createQueryKeys output:
// {
//   _def: ['users'],
//   detail: (userId: string) => {
//     queryKey: ['users', 'detail', userId],
//     queryFn: (ctx: QueryFunctionContext) => api.getUser(userId),
//   },
// }

export function useUserDetail(id: string) {
  return useQuery(users.detail(id));
};

Generate contextual queries

Declare queries that are dependent or related to a parent context (e.g.: all likes from a user):

export const users = createQueryKeys('users', {
  detail: (userId: string) => ({
    queryKey: [userId],
    queryFn: () => api.getUser(userId),
    contextQueries: {
      likes: {
        queryKey: null,
        queryFn: () => api.getUserLikes(userId),
      },
    },
  }),
});

// => createQueryKeys output:
// {
//   _def: ['users'],
//   detail: (userId: string) => {
//     queryKey: ['users', 'detail', userId],
//     queryFn: (ctx: QueryFunctionContext) => api.getUser(userId),
//     _ctx: {
//       likes: {
//         queryKey: ['users', 'detail', userId, 'likes'],
//         queryFn: (ctx: QueryFunctionContext) => api.getUserLikes(userId),
//       },
//     },
//   },
// }

export function useUserLikes(userId: string) {
  return useQuery(users.detail(userId)._ctx.likes);
};

Access to serializable keys scope definition

Easy way to access the serializable key scope and invalidate all cache for that context:

users.detail(userId).queryKey; // => ['users', 'detail', userId]
users.detail._def; // => ['users', 'detail']

Create a single point of access for all your query keys

Declare your query keys store in a single file

Just one place to edit and maintain your store:

export const queries = createQueryKeyStore({
  users: {
    all: null,
    detail: (userId: string) => ({
      queryKey: [userId],
      queryFn: () => api.getUser(userId),
    }),
  },
  todos: {
    detail: (todoId: string) => [todoId],
    list: (filters: TodoFilters) => ({
      queryKey: [{ filters }],
      queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
    }),
  },
});

Declare your query keys by feature

Have fine-grained control over your features' keys and merge them into a single object to have access to all your query keys in your codebase:

// queries/users.ts
export const users = createQueryKeys('users', {
  all: null,
  detail: (userId: string) => ({
    queryKey: [userId],
    queryFn: () => api.getUser(userId),
  }),
});

// queries/todos.ts
export const todos = createQueryKeys('todos', {
  detail: (todoId: string) => [todoId],
  list: (filters: TodoFilters) => ({
    queryKey: [{ filters }],
    queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
  }),
});

// queries/index.ts
export const queries = mergeQueryKeys(users, todos);

Type safety and smart autocomplete

Typescript is a first class citizen of Query Key Factory, providing easy of use and autocomplete for all query keys available and their outputs. Don't remember if a key is serializable or the shape of a key? Just let your IDE show you all information you need.

Infer the type of the store's query keys

import { createQueryKeyStore, inferQueryKeyStore } from "@lukemorales/query-key-factory";

export const queries = createQueryKeyStore({
  /* ... */
});

export type QueryKeys = inferQueryKeyStore<typeof queries>;
// queries/index.ts
import { mergeQueryKeys, inferQueryKeyStore } from "@lukemorales/query-key-factory";

import { users } from './users';
import { todos } from './todos';

export const queries = mergeQueryKeys(users, todos);

export type QueryKeys = inferQueryKeyStore<typeof queries>;

Infer the type of a feature's query keys

import { createQueryKeys, inferQueryKeys } from "@lukemorales/query-key-factory";

export const todos = createQueryKeys('todos', {
  detail: (todoId: string) => [todoId],
  list: (filters: TodoFilters) => ({
    queryKey: [{ filters }],
    queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
  }),
});

export type TodosKeys = inferQueryKeys<typeof todos>;

Type your QueryFunctionContext with ease

Get accurate types of your query keys passed to the queryFn context:

import type { QueryKeys } from "../queries";
// import type { TodosKeys } from "../queries/todos";

type TodosList = QueryKeys['todos']['list'];
// type TodosList = TodosKeys['list'];

const fetchTodos = async (ctx: QueryFunctionContext<TodosList['queryKey']>) => {
  const [, , { filters }] = ctx.queryKey;

  return api.getTodos({ filters, page: ctx.pageParam });
}

export function useTodos(filters: TodoFilters) {
  return useQuery({
    ...queries.todos.list(filters),
    queryFn: fetchTodos,
  });
};