query-stuff
TypeScript icon, indicating that this package has built-in type declarations

0.2.0 • Public • Published

query-stuff

Type-safe query/mutation factories for @tanstack/query

👪 All Contributors: 1 📝 License: MIT 📦 npm version 💪 TypeScript: Strict

📖 Table of Contents

🎯 Motivation

query-stuff builds on top of ideas from TkDodo’s blog, particularly Effective React Query Keys and The Query Options API.

📦 Installation

query-stuff requires @tanstack/react-query v5 and above as a peerDependency.

npm i @tanstack/react-query query-stuff

🚀 Features

  • 🏭 factory

    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 a factory 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"]

    ⚙ Options

    • 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.

    💬 Note

    • name will be used as the base key for all queries, mutations, and groups within the factory.
  • 📥 query

    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"]

    ⚙ Options

    • 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 and queryKey are handled internally and should not be included here. Refer to the official useQuery docs for all the available options.

    💬 Note

    • queryKey is automatically generated for each query.
  • 📤 mutation

    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"]

    ⚙ Options

    • 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 and mutationKey are handled internally and should not be included here. Refer to official useMutation docs for all the available options.

    💬 Note

    • mutationKey is automatically generated for each mutation.
  • 📝 input

    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 }]

    ⚙ Options

    • schema: Schema | RecordSchema

    💬 Note

    • Refer to the official "What schema libraries implement the spec?" docs for compatible schema libraries.
    • RecordSchema can used as input for query, mutation and group.
    • Schema can used as input for queryand mutation only.
  • 📦 group

    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 }]

    ⚙ Options

    • builderFn:
      • Required
      • A function that receives a builder and returns an object containing queries, mutations, and groups.

    💬 Note

    • group with an input can only created with a RecordSchema.
  • 🔗 use (🧪 Experimental)

    The use function allows composing middlewares that wrap outgoing functions, such as queryFn for queries and mutationFn for mutations.

    🧪 Experimental

    • 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) => ({
          // ...
        })),
    }));

    ⚙ Options

    • middlewareFn:

      • Required
      • A function that receives a next function and a ctx object, then returns the result from calling next.
      • ctx is the incoming context object.
      • next is a function that accepts an object with an extended ctx and returns the result of the execution chain.

    💬 Note

    • The next function can be used to extend the outgoing context with a new ctx object.
    • The result of the next function must be returned.
  • 🔄 useExactMutationState

    The useExactMutationState hook is built on top of React Query's useMutationState hook. The useExactMutationState hook provides a type-safe API for tracking mutations for a given mutationKey provided by query-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

    • options:
      • filters: MutationFilters
        • Required
        • mutationKey:
          • Required
          • The mutationKey for a given mutation. Must be retrieved from query-stuff
        • Additional mutation filters:
          • Optional
          • Refer to the official Mutation Filters docs for the available options.
    • select: (mutation: Mutation) => TResult
      • Optional
      • Use this to transform the mutation state.

    💬 Note

    • The mutationKey must be retrieved directly from query-stuff.
    • The exact filter is set to true by default.
  • 🏗️ middlewareBuilder (🧪 Experimental)

    🧪 Experimental

    • 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 the use 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
      }),
    }));

    ⚙ Options

    • middlewareFn:

      • Required
      • A function that receives a next function and a ctx object, then returns the result from calling next.
      • ctx is the incoming context object.
      • next is a function that accepts an object with an extended ctx and returns the result of the execution chain.

    💬 Note

    • The next function can be used to extend the outgoing context with a new ctx object.
    • The result of the next function must be returned.
  • 🔧 extend (🧪 Experimental)

    🧪 Experimental

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

    ⚙ Options

    • middlewareFn:

      • Required
      • A function that receives a next function and a ctx object, then returns the result from calling next.
      • ctx is the incoming context object.
      • next is a function that accepts an object with an extended ctx and returns the result of the execution chain.

    💬 Note

    • The next function can be used to extend the outgoing context with a new ctx 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.
  • 🧬 inherit (🧪 Experimental)

    🧪 Experimental

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

    ⚙ Options

    • middlewareFn:

      • Required
      • A function that receives a next function and a ctx object, then returns the result from calling next.
      • ctx is the incoming context object.
      • next is a function that accepts an object with an extended ctx and returns the result of the execution chain.

    💬 Note

    • The next function can be used to extend the outgoing context with a new ctx 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.

🙏 Credits

📜 License

MIT © MlNl-PEKKA

Package Sidebar

Install

npm i query-stuff

Weekly Downloads

4

Version

0.2.0

License

MIT

Unpacked Size

84.8 kB

Total Files

27

Last publish

Collaborators

  • mlnl-pekka