Type-safe query/mutation factories for @tanstack/query
- 🎯 Motivation
- 📦 Installation
-
🚀 Features
-
🏭 factory
- 📥 query
- 📤 mutation
- 📝 input
- 📦 group
- 🔗 use - 🧪 Experimental
- 🔄 useExactMutationState
- 🏗️ middlewareBuilder - 🧪 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
factory
function 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 afactory
for 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.
-
name
will 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.
queryFn
andqueryKey
are handled internally and should not be included here. Refer to the official useQuery docs for all the available options.
-
queryKey
is 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.
mutationFn
andmutationKey
are handled internally and should not be included here. Refer to official useMutation docs for all the available options.
-
mutationKey
is 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.
-
RecordSchema
can used as input forquery
,mutation
andgroup
. -
Schema
can used as input forquery
andmutation
only.
-
-
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.
-
group
with aninput
can only created with aRecordSchema
.
-
-
The
use
function allows composing middlewares that wrap outgoing functions, such asqueryFn
for queries andmutationFn
for 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
next
function and actx
object, then returns the result from callingnext
. -
ctx
is the incoming context object. -
next
is a function that accepts an object with an extendedctx
and returns the result of the execution chain.
- The
next
function can be used to extend the outgoing context with a newctx
object. - The result of the
next
function must be returned.
- This feature is experimental and prefixed with
-
The
useExactMutationState
hook is built on top of React Query's useMutationState hook. TheuseExactMutationState
hook provides a type-safe API for tracking mutations for a givenmutationKey
provided 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
mutationKey
for 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
mutationKey
must be retrieved directly fromquery-stuff
. - The
exact
filter is set to true by default.
-
-
- This feature is experimental and prefixed with
unstable_
. - This API may change in future versions.
The
middlewareBuilder
allows defining reusable, standalone middleware functions that can be plugged into theuse
function.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
next
function and actx
object, then returns the result from callingnext
. -
ctx
is the incoming context object. -
next
is a function that accepts an object with an extendedctx
and returns the result of the execution chain.
- The
next
function can be used to extend the outgoing context with a newctx
object. - The result of the
next
function 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
extend
function 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
next
function and actx
object, then returns the result from callingnext
. -
ctx
is the incoming context object. -
next
is a function that accepts an object with an extendedctx
and returns the result of the execution chain.
- The
next
function can be used to extend the outgoing context with a newctx
object. - The result of the
next
function must be returned. -
extend
stacks 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
inherit
function 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
next
function and actx
object, then returns the result from callingnext
. -
ctx
is the incoming context object. -
next
is a function that accepts an object with an extendedctx
and returns the result of the execution chain.
- The
next
function can be used to extend the outgoing context with a newctx
object. - The result of the
next
function must be returned. -
inherit
does 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