Usage
Managing State

External libraries

The builder object is useful for generating keys for asynchronous state management libraries, such as Tanstack Query (opens in a new tab) and SWR (Stale-While-Revalidate (opens in a new tab)). This eliminates the need to manually construct keys for each query, and reduces the risk of inconsistencies and conflicts.

The values of the nested keys in the builder object can be made function queries, making it a documentation for the various endpoints to any given API. This approach simplifies code maintenance, synchronizing values, generating keys, refactoring, and managing cache invalidation, as the builder object becomes the single source of truth for all queries.

The following code snippet demonstrates how to define an api builder using createBuilder:

type List = {
  data: {
    id: number;
    name: string;
  }[];
};
 
const register = {
  retailers: {
    list: async () => {
      return axios.get<List>("/api/retailers");
    },
    delete: async (params: { id: number }) => {
      return axios.delete<{ message: string }>({
        url: `/api/retailers/${params.id}`,
      });
    },
  },
  products: {
    list: async () => {
      const resp = await fetch("/api/products");
      return resp.json();
    },
    delete: async (params: { id: number }) => {
      const resp = await fetch(`/api/products/${params.id}`, {
        method: "DELETE",
      });
      return resp.json();
    },
  },
};
 
const api = createBuilder(register);

Queries

The api object above contains two endpoints, retailers and products, each with a list and delete function query. The list function query is used to retrieve a list of items, while the delete function query is used to delete an item.

Using the $use method

The $use method is used to retrieve the query key because although the list function is defined as a function query, it does not accept any parameters.

const { data: retailers } = useQuery({
  queryKey: api.retailers.list.$use(),
  // ^? ["retailers", "list"]
  queryFn: api.$use.retailers.list
});

Using the $get method

It is common to use the $use method to retrieve the query key, as it is the most common use case. However, the $get method can also be used to retrieve the query key. And in some cases, it is more suitable to use the $get method when we want to include arbitrary parameters in the query key.

const { data: products } = useQuery({
  queryKey: api.products.list.$get(4, ['my', 'shelf']),
  // ^? ["products", "list", 4, ['my', 'shelf'] ]
  queryFn: api.$use.products.list
});

Mutations

Mutations are used to update or delete data on the server. The delete function query is used to delete an item from the server. The api.retailers.delete node is a function that expects an object with an id property, as an argument.

Using the $use method

In the following example, the $use method is used to retrieve the mutation key. The $use method expects the same number of arguments as the function query.

const { mutate } = useMutation({
  mutationKey: api.products.delete.$use({ id: 5 }),
  // ^? ["products", "delete", { id: 5 }]
  mutationFn: () => api.$use.products.delete({ id: 5 })
});

Using the $get method

If, for some reason, you want to pass the argument in a different way, you can use the $get method to retrieve the mutation key.

const { mutate } = useMutation({
  mutationKey: api.retailers.delete.$get(5, "alive"),
  // ^? ["retailers", "delete", 5, "alive"]
  mutationFn: () => api.$use.retailers.delete({ id: 5 })
});

Missing dependencies

Due to how the $use method is constructed, it is impossible to pass the wrong number of arguments to the function. Unless the function is not a TypeScript (opens in a new tab) function, within a strict TypeScript (opens in a new tab) environment, or if the function is defined in such a way that allows it. For example, using optional parameters in the function query.

type State = "active" | "completed" | "archived";
type Sorting = "dateCreated" | "name";
type Todos = {
  data: {
    title: string;
    description: string;
    created: string;
    state: State;
  }[];
};
 
const api = createBuilder({
  todos: {
    list: async (state: State, sorting?: Sorting) => {
      const resp = await axios.get<Todos>(`todos/${state}?sorting=${sorting}`);
      return resp.data;
    },
  },
});

Error handling

In the api builder above, the api.todos.list function query accepts two parameters, state and sorting. The state parameter is required, while the sorting parameter is optional. The $use method is used to retrieve the query key, and it expects the same number of required arguments as the function query. Not passing the required amount of arguments will result in a type error (opens in a new tab).

const [state, setState] = useState<State>('active');
 
const { data: todos } = useQuery({
  queryKey: api.todos.list.$use(state),
  // ^? ["todos", "list", "active", undefined]
  queryFn: () => api.$use.todos.list(state)
});