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