react-query-kit

repository·main·Indexed 19 days ago

https://github.com/huolalatech/react-query-kit

A toolkit for TanStack React Query (version 3.3.4) that makes queries, infinite queries, and mutations reusable and type-safe via centralized definitions. It provides utilities like createQuery, createInfiniteQuery, createMutation, and a router function for hierarchical API shapes. Features include middleware support for intercepting hooks, type extraction utilities (inferData, inferVariables), and specialized wrappers for React Suspense.

Tokens
8.6K
Snippets
25
Records
30
Agent score
66%

What's inside react-query-kit

  1. Define an API shape using `router`

    main

    The router function allows you to define the entire shape of your API in a hierarchical structure. You can nest routers to create organized API endpoints. Each node in the router can define queries, infinite queries, or mutations.

    Key features:

    • Hierarchical Keys: Automatically manages nested query keys.
    • Integrated Hooks: Each definition provides ready-to-use hooks like useQuery, useInfiniteQuery, and useMutation.
    • Type Inference: Automatically infers data and variable types from the fetcher or mutationFn.

    Common methods available on router nodes:

    • getKey(variables?): Returns the query key array.
    • getOptions(variables?): Returns TanStack Query options.
    • getFetchOptions(variables?): Returns only the necessary options for fetching (omits staleTime, retry, etc.).
    • fetcher(variables?): Returns the fetcher function.
    import { router } from 'react-query-kit'
    
    const post = router(`post`, {
      byId: router.query({
        fetcher: (variables: { id: number }) =>
          fetch(`/posts/${variables.id}`).then(res => res.json()),
      }),
    
      list: router.infiniteQuery({
        fetcher: (_variables, { pageParam }) =>
          fetch(`/posts/?cursor=${pageParam}`).then(res => res.json()),
        getNextPageParam: lastPage => lastPage.nextCursor,
        initialPageParam: 0,
      }),
    
      add: router.mutation({
        mutationFn: async (variables: { title: string; content: string }) =>
          fetch('/posts', { 
            method: 'POST', 
            body: JSON.stringify(variables) 
          }).then(res => res.json()),
      }),
    
      // Nesting a router
      command: router(`command`, {
        report: router.mutation({ mutationFn: ... }),
      }),
    })
    
    // Usage
    post.byId.useQuery({ variables: { id: 1 } })
    post.list.useInfiniteQuery()
    post.add.useMutation()
  2. Use Middleware to execute logic before and after hooks

    main

    Middleware in ReactQueryKit allows you to intercept and wrap hooks to execute logic before or after they run. This is inspired by SWR's middleware pattern.

    How it works

    • Middleware receives the useQueryNext (or useMutationNext) function.
    • It returns a new function that wraps the original hook.
    • If multiple middlewares are provided in the use array, they wrap each other in the order they are listed (e.g., [a, b, c] executes as a -> b -> c -> hook).

    Global vs Local Middleware

    • Local: Defined in the use option of a specific createQuery or router node.
    • Global: Defined in the QueryClient defaultOptions for queries or mutations.
    import { QueryClient } from '@tanstack/react-query'
    import { Middleware, QueryHook } from 'react-query-kit'
    
    // 1. Define a middleware
    const logger: Middleware<QueryHook> = useQueryNext => {
      return options => {
        const fetcher = (variables, context) => {
          console.log('Fetching:', context.queryKey, variables)
          return options.fetcher(variables, context)
        }
    
        return useQueryNext({
          ...options,
          fetcher,
        })
      }
    }
    
    // 2. Apply globally via QueryClient
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          use: [logger],
        },
      },
    })
  3. Disable queries using `skipToken`

    main

    To prevent a query from executing (similar to conditional fetching), pass skipToken from @tanstack/react-query as the variables option. This is particularly useful when your variables depend on an async value or a state that might be undefined.

    This works for both individual hooks and useQueries.

    import { skipToken } from '@tanstack/react-query'
    
    // In a component
    const [id, setId] = useState<string | undefined>()
    
    // The query will not run until `id` is defined
    const { data } = usePost({
      variables: id ? { id } : skipToken,
    })
    
    // Using with useQueries
    const queries = useQueries({
      queries: [usePost.getOptions(id ? { id } : skipToken)],
    })
  4. Use Middleware to extend Query Hooks

    main

    Middleware allows you to intercept and wrap query or mutation hooks. A Middleware is a higher-order function that takes a hook and returns a new hook with the same signature.

    export type Middleware<T extends (...args: any) => any = QueryHook<any, any, any>> = (
      hook: (options: inferCreateOptions<T>, queryClient?: QueryClient) => ReturnType<T>
    ) => (options: inferCreateOptions<T>, queryClient?: QueryClient) => ReturnType<T>
    export type Middleware<
      T extends (...args: any) => any = QueryHook<any, any, any>
    > = (hook: inferMiddlewareHook<T>) => inferMiddlewareHook<T>
  5. Understand the QueryHook interface

    main

    A QueryHook is the primary consumer-facing object created by the library. It acts as both a React hook and a utility object that exposes methods to interact with the underlying query without needing to call the hook.

    As a Hook: It can be called in a component to return query results (data, error, status, etc.). It supports both standard and Defined versions (where initialData is required).

    As a Utility Object (ExposeMethods):

    • fetcher: A function to manually trigger the fetcher with specific variables.
    • getKey(variables?): Returns the QueryKey associated with the query, optionally scoped by variables.
    • getOptions(variables?): Returns the configuration options (like queryKey, queryFn, etc.) used by the hook.
    • getFetchOptions(variables?): Returns a subset of options specifically for fetching (e.g., queryKey, queryFn, staleTime).
    export interface QueryHook<TFnData = unknown, TVariables = void, TError = CompatibleError> extends ExposeMethods<TFnData, TVariables, TError> {
      <TData = TFnData>(
        options: DefinedQueryHookOptions<TFnData, TError, TData, TVariables>,
        queryClient?: QueryClient
      ): DefinedUseQueryResult<TData, TError>
      <TData = TFnData>(
        options?: QueryHookOptions<TFnData, TError, TData, TVariables>,
        queryClient?: QueryClient
      ): UseQueryResult<TData, TError>
    }
  6. Configure the Router pattern

    main

    The Router pattern allows you to define a structured, nested configuration of queries and mutations. This is useful for organizing data requirements in a centralized way.

    • RouterConfig: A recursive object structure where keys can be RouterLeaf (a query or mutation) or another RouterConfig.
    • CreateRouter<TConfig>: A type-level utility that transforms a raw RouterConfig into a ResolvedRouter object. The resolved object provides typed access to the hooks (e.g., useQuery, useMutation) and a getKey() method to retrieve the global QueryKey for the entire router configuration.
  7. Migrate from ReactQueryKit 2 to 3

    main

    In version 3, the API has shifted from primaryKey and queryFn to a more streamlined queryKey and fetcher pattern. The fetcher automatically handles the mapping of variables to the query key and the query function.

    Old Pattern (v2):

    createQuery({
      primaryKey: 'posts',
      queryFn: ({ queryKey: [_primaryKey, variables] }) => { ... },
    })

    New Pattern (v3):

    createQuery({
      queryKey: ['posts'],
      fetcher: variables => { ... },
    })
  8. Use createSuspenseQuery and createSuspenseInfiniteQuery

    main

    These functions are wrappers around createQuery and createInfiniteQuery that automatically set enabled: true, suspense: true, and throwOnError: true. This is particularly useful for TypeScript because it guarantees that data is defined within the component, as loading and error states are handled by React Suspense and Error Boundaries.

    import { createSuspenseQuery, createSuspenseInfiniteQuery } from 'react-query-kit'
    
    // For standard queries
    const usePost = createSuspenseQuery({
      queryKey: ['posts'],
      fetcher: () => fetch('/posts').then(res => res.json()),
    })
    
    // For infinite queries
    const useProjects = createSuspenseInfiniteQuery({
      queryKey: ['projects'],
      fetcher: () => fetch('/projects').then(res => res.json()),
      getNextPageParam: (lastPage) => lastPage.nextCursor,
      initialPageParam: 0,
    })
  9. Use createInfiniteQuery for infinite scrolling

    main

    createInfiniteQuery is used for paginated data fetching. It manages pageParam and provides the necessary helpers for infinite scrolling.

    Options

    • fetcher: (Required) Receives variables and context (which includes pageParam).
    • variables: (Optional) Variables appended to the queryKey.
    • getNextPageParam: Function to determine the next cursor/page.
    • initialPageParam: The starting page parameter.
    • use: (Optional) Middleware array.

    Exposed Methods

    • getKey(variables): Returns the QueryKey.
    • getOptions(variables): Returns UseInfiniteQueryOptions.
    • getFetchOptions(variables): Returns options for prefetchInfiniteQuery or fetchInfiniteQuery outside of React.

    Example

    import { createInfiniteQuery } from 'react-query-kit'
    
    type Data = { projects: { id: string; name: string }[]; nextCursor: number }
    type Variables = { active: boolean }
    
    const useProjects = createInfiniteQuery({
      queryKey: ['projects'],
      fetcher: (variables: Variables, { pageParam }): Promise<Data> => {
        return fetch(`/projects?cursor=${pageParam}?active=${variables.active}`).then(res => res.json())
      },
      getNextPageParam: (lastPage) => lastPage.nextCursor,
      initialPageParam: 0,
    })
    
    // Inside a component
    const { data, fetchNextPage, hasNextPage } = useProjects({ variables: { active: true } })
  10. Use createMutation for data mutations

    main

    createMutation provides a type-safe way to define mutations. It exposes a hook that provides standard mutation states (isPending, isError, isSuccess) and methods.

    Options

    • mutationFn: (Required) The function that performs the mutation.
    • onSuccess, onError, onSettled: Lifecycle callbacks.
    • use: (Optional) Middleware array.

    Exposed Methods

    • getKey(): Returns the MutationKey.
    • getOptions(): Returns UseMutationOptions.
    • mutationFn: An exposed version of the mutation function that can be called outside of React components.

    Example

    import { createMutation } from 'react-query-kit'
    
    const useAddTodo = createMutation({
      mutationFn: async (variables: { title: string; content: string }) =>
        fetch('/post', {
          method: 'POST',
          body: JSON.stringify(variables),
        }).then(res => res.json()),
    })
    
    function App() {
      const mutation = useAddTodo()
    
      return (
        <button
          onClick={() => {
            mutation.mutate({ title: 'Do Laundry', content: 'content...' })
          }}
        >
          create Todo
        </button>
      )
    }
    
    // Usage outside of react component
    useAddTodo.mutationFn({ title: 'Do Laundry', content: 'content...' })
  11. Extract TypeScript types with `inferData` and `inferVariables`

    main

    ReactQueryKit provides utility functions to extract TypeScript types from your custom hooks or router definitions. This is useful for maintaining type safety in components that consume these hooks.

    Available utilities:

    • inferData<T>: Extracts the data type (e.g., InfiniteData<T> for infinite queries).
    • inferFnData<T>: Extracts the raw data type returned by the fetcher function.
    • inferVariables<T>: Extracts the variables type.
    • inferError<T>: Extracts the error type.
    • inferOptions<T>: Extracts the hook options type.
    import { inferData, inferVariables, inferError } from 'react-query-kit'
    
    const useProjects = createInfiniteQuery<Data, Variables, Error>(...)
    
    type Data = inferData<typeof useProjects> // InfiniteData<Data>
    type Vars = inferVariables<typeof useProjects> // Variables
    type Err = inferError<typeof useProjects> // Error