Type-safe query/mutation factories for @tanstack/query
- π― Motivation
- π¦ Installation
- π Features
- π factory
- π₯ query
- π€ mutation
- π input
- π¦ group
- π use - π§ͺ Experimental
- π useExactMutationState
- ποΈ middlewareBuilder - π§ͺ Experimental
- π§ extend - π§ͺ Experimental
- 𧬠inherit - π§ͺ Experimental
- π factory
- π Credits
- π License
query-stuff builds on top of ideas from TkDodoβs blog, particularly Effective React Query Keys and The Query Options API.
query-stuff requires @tanstack/react-query v5 and above as a peerDependency.
npm i @tanstack/react-query query-stuff-
The
factoryfunction is the core of query-stuff. It provides a structured way to define queries, mutations, and groups without manually managing their respective keys. It is recommended to have afactoryfor each feature as stated in the Use Query Key factories section of the Effective React Query blog.Setup:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ /* Creates an empty todos factory */ }));
Usage:
console.log(todos._key); // ["todos"]
name:string- Required
- The name of the feature.
builderFn:- Required
- A function that receives a builder and returns an object containing queries, mutations, and groups.
namewill be used as the base key for all queries, mutations, and groups within the factory.
-
Setup:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ read: q.query( async () => { /*Fetch all todos*/ }, { staleTime: 0, gcTime: 5 * 60 * 1000, }, ), }));
Usage:
useQuery(todos.read()); console.log(todos.read().queryKey); // ["todos", "read"]
-
queryFn:({ ctx: Context | void, input: Input | void }) => Promise<Data>- Required
- The function that the query will use to request data.
-
options:- Optional
- Additional options for the query.
queryFnandqueryKeyare handled internally and should not be included here. Refer to the official useQuery docs for all the available options.
queryKeyis automatically generated for each query.
-
-
Setup:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ // ... delete: q.mutation( async () => { //Delete all todos }, { onMutate: () => { //onMutate }, onSuccess: () => { //onSuccess }, onError: () => { //onError }, }, ), }));
Usage:
useMutation(todos.delete()); console.log(todos.delete().mutationKey); // ["todos", "delete"]
mutationFn:({ ctx: Context| void, input: Input | void }) => Promise<Data>- Required
- A function that performs an asynchronous task and returns a promise.
options:- Optional
- Additional options for the mutation.
mutationFnandmutationKeyare handled internally and should not be included here. Refer to official useMutation docs for all the available options.
mutationKeyis automatically generated for each mutation.
-
Setup:
import { factory } from "query-stuff"; import { z } from "zod"; const todos = factory("todos", (q) => ({ // ... todo: q.input(z.object({ id: z.number() })).query(({ input }) => { /*Fetch a todo*/ }), }));
Usage:
useQuery(todos.todo({ id: 1 })); console.log(todos.todo({ id: 1 }).queryKey); // ["todos", "todo", { id: 1 }]
schema:Schema | RecordSchema- Required
- A Standard Schema compliant schema.
- Refer to the official "What schema libraries implement the spec?" docs for compatible schema libraries.
RecordSchemacan used as input forquery,mutationandgroup.Schemacan used as input forqueryandmutationonly.
-
Setup:
import { factory } from "query-stuff"; import { z } from "zod"; const todos = factory("todos", (q) => ({ // ... todo: q.input(z.object({ id: z.number() })).group((q) => ({ read: q.query(({ ctx }) => { /*Fetch todo*/ }), delete: q.mutation(({ ctx }) => { /*Delete todo*/ }); })), }));
Usage:
useQuery(todos.todo({ id: 1 }).read()); console.log(todos.todo({ id: 1 }).read().queryKey); // ["todos", "todo", { id: 1 }, "read"] useMutation(todos.todo({ id: 1 }).delete()); console.log(todos.todo({ id: 1 }).delete().mutationKey); // ["todos", "todo", { id: 1 }, "delete"] console.log(todos.todo._key); // ["todos", "todo"] console.log(todos.todo({ id: 1 })._key); // ["todos", "todo", { id: 1 }]
builderFn:- Required
- A function that receives a builder and returns an object containing queries, mutations, and groups.
groupwith aninputcan only created with aRecordSchema.
-
The
usefunction allows composing middlewares that wrap outgoing functions, such asqueryFnfor queries andmutationFnfor mutations.- This feature is experimental and prefixed with
unstable_. - This API may change in future versions.
Setup/Usage:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ // ... todo: q .unstable_use(async ({ next, ctx }) => { // Before const start = Date.now(); const result = await next({ ctx: { /*Extended context */ }, }); const end = Date.now(); console.log(`Elapsed time: ${end - start}`); // After return result; }) .group((q) => ({ // ... })), }));
-
middlewareFn:- Required
- A function that receives a
nextfunction and actxobject, then returns the result from callingnext. ctxis the incoming context object.nextis a function that accepts an object with an extendedctxand returns the result of the execution chain.
- The
nextfunction can be used to extend the outgoing context with a newctxobject. - The result of the
nextfunction must be returned.
- This feature is experimental and prefixed with
-
The
useExactMutationStatehook is built on top of React Query's useMutationState hook. TheuseExactMutationStatehook provides a type-safe API for tracking mutations for a givenmutationKeyprovided byquery-stuff.Setup:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ // ... delete: q.mutation(async () => { //Delete all todos }), }));
Usage:
useMutation(todos.delete()); useExactMutationState({ filters: { mutationKey: todos.delete().mutationKey, }, select: (mutation) => mutation.state.data, });
options:- filters:
MutationFilters- Required
- mutationKey:
- Required
- The
mutationKeyfor a given mutation. Must be retrieved fromquery-stuff
- Additional mutation filters:
- Optional
- Refer to the official Mutation Filters docs for the available options.
- filters:
select:(mutation: Mutation) => TResult- Optional
- Use this to transform the mutation state.
- The
mutationKeymust be retrieved directly fromquery-stuff. - The
exactfilter is set to true by default.
-
- This feature is experimental and prefixed with
unstable_. - This API may change in future versions.
The
middlewareBuilderallows defining reusable, standalone middleware functions that can be plugged into theusefunction.Setup:
import { unstable_middlewareBuilder } from "query-stuff"; const { middleware: fooMiddleware } = unstable_middlewareBuilder( async ({ next }) => { return await next({ ctx: { foo: "foo", }, }); }, );
Before:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ read: q .unstable_use(async ({ next }) => { return await next({ ctx: { foo: "foo", }, }); }) .query(async ({ ctx }) => { console.log(ctx); // { foo: "foo" } // Fetch all todos }), }));
After:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ read: q.unstable_use(fooMiddleware).query(async ({ ctx }) => { console.log(ctx); // { foo: "foo" } // Fetch all todos }), }));
-
middlewareFn:- Required
- A function that receives a
nextfunction and actxobject, then returns the result from callingnext. ctxis the incoming context object.nextis a function that accepts an object with an extendedctxand returns the result of the execution chain.
- The
nextfunction can be used to extend the outgoing context with a newctxobject. - The result of the
nextfunction must be returned.
- This feature is experimental and prefixed with
-
- This feature is experimental and prefixed with
unstable_. - This API may change in future versions.
The
extendfunction allows you to build upon an existing standalone middleware, adding additional transformations while preserving previous ones.Setup:
import { unstable_middlewareBuilder } from "query-stuff"; const { middleware: fooMiddleware } = unstable_middlewareBuilder( async ({ next }) => { return await next({ ctx: { foo: "foo", }, }); }, ); const { middleware: fooBarMiddleware } = unstable_middlewareBuilder( fooMiddleware, ).unstable_extend(async ({ next, ctx }) => { console.log(ctx); // { foo: "foo" } return await next({ ctx: { bar: "bar", }, }); });
Before:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ read: q .unstable_use(async ({ next }) => { return await next({ ctx: { foo: "foo", }, }); }) .unstable_use(async ({ next, ctx }) => { console.log(ctx); // { foo: "foo" } return await next({ ctx: { bar: "bar", }, }); }) .query(async ({ ctx }) => { console.log(ctx); // { foo: "foo", bar: "bar" } // Fetch all todos }), }));
After:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ read: q.unstable_use(fooBarMiddleware).query(async ({ ctx }) => { console.log(ctx); // { foo: "foo", bar: "bar" } // Fetch all todos }), }));
-
middlewareFn:- Required
- A function that receives a
nextfunction and actxobject, then returns the result from callingnext. ctxis the incoming context object.nextis a function that accepts an object with an extendedctxand returns the result of the execution chain.
- The
nextfunction can be used to extend the outgoing context with a newctxobject. - The result of the
nextfunction must be returned. extendstacks middlewares in the order they are defined, ensuring each middleware in the chain is executed sequentially.
- This feature is experimental and prefixed with
-
- This feature is experimental and prefixed with
unstable_. - This API may change in future versions.
The
inheritfunction allows you to build a new middleware that is anticipated as an upcoming middleware with respect to a given source middleware. This allows you to create standalone middlewares while maintaining type safety and context awareness.Setup:
import { unstable_middlewareBuilder } from "query-stuff"; const { middleware: fooMiddleware } = unstable_middlewareBuilder( async ({ next }) => { return await next({ ctx: { foo: "foo", }, }); }, ); const { middleware: barMiddleware } = unstable_middlewareBuilder( fooMiddleware, ).unstable_inherit(async ({ next, ctx }) => { console.log(ctx); // { foo: "foo" } return await next({ ctx: { bar: "bar", }, }); });
Before:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ read: q .unstable_use(async ({ next }) => { return await next({ ctx: { foo: "foo", }, }); }) .unstable_use(async ({ next, ctx }) => { console.log(ctx); // { foo: "foo" } return await next({ ctx: { bar: "bar", }, }); }) .query(async ({ ctx }) => { console.log(ctx); // { foo: "foo", bar: "bar" } // Fetch all todos }), }));
After:
import { factory } from "query-stuff"; const todos = factory("todos", (q) => ({ read: q .unstable_use(fooMiddleware) .unstable_use(barMiddleware) .query(async ({ ctx }) => { console.log(ctx); // { foo: "foo", bar: "bar" } // Fetch all todos }), }));
-
middlewareFn:- Required
- A function that receives a
nextfunction and actxobject, then returns the result from callingnext. ctxis the incoming context object.nextis a function that accepts an object with an extendedctxand returns the result of the execution chain.
- The
nextfunction can be used to extend the outgoing context with a newctxobject. - The result of the
nextfunction must be returned. inheritdoes not modify the middleware execution order.- It must only be used to ensure type safety and context awareness when composing middleware.
- This feature is experimental and prefixed with
-
As mentioned in the motivation section, this library enforces best practices outlined in TkDodoβs blog, particularly Effective React Query Keys and The Query Options API.
-
The API design is heavily inspired by tRPC, particularly its use of the builder pattern and intuitive method conventions.
MIT Β© MlNl-PEKKA