Jotai Query (jotai-tanstack-query)

repository·main·Indexed 43 days ago

https://github.com/jotaijs/jotai-tanstack-query

A bridge between Jotai and TanStack Query that enables managing asynchronous server state using Jotai atoms. It provides utilities like atomWithQuery, atomWithMutation, atomWithInfiniteQuery, and atomWithQueries to integrate TanStack Query's data fetching, caching, and synchronization features directly into a Jotai state management workflow.

Tokens
9.6K
Snippets
22
Records
36
Agent score
52%

What's inside Jotai Query

  1. Overview of Jotai Query

    main
    Jotai Query (jotai-tanstack-query) is an extension library for Jotai that integrates with TanStack Query. It allows you to use TanStack Query's powerful data fetching, caching, and synchronization features directly within your Jotai state management workflow.
  2. How to achieve render optimization with `atomWithQuery`

    main

    Unlike TanStack Query's React hooks, atomWithQuery does not implement "tracked properties" (where a component only re-renders when a specific accessed property changes). Instead, subscribing to the atomWithQuery atom will trigger a re-render whenever any part of the query result changes (e.g., isFetching, status, data).

    To achieve the same optimization as TanStack Query's tracked properties, use the derived atom pattern. Create a new atom that selects only the specific property you need. Components subscribing to this derived atom will only re-render when that specific property changes.

    const queryAtom = atomWithQuery(...)
    
    // Create a derived atom for specific data
    const dataAtom = atom((get) => get(queryAtom).data)
    
    const Component = () => {
      // This component only re-renders when 'data' changes,
      // ignoring changes to 'isFetching' or 'status'.
      const data = useAtomValue(dataAtom)
      ...
    }
    const queryAtom = atomWithQuery(...)
    
    const dataAtom = atom((get) => get(queryAtom).data)
    
    const Component = () => {
      const data = useAtomValue(dataAtom)
      ...
    }
  3. Configure TanStack Query Devtools

    main

    To use the Devtools, install @tanstack/react-query-devtools as a dev dependency and place the <ReactQueryDevtools /> component inside your <QueryClientAtomProvider />.

    $ npm i @tanstack/react-query-devtools --save-dev
    import { QueryClient } from '@tanstack/react-query'
    import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
    import { QueryClientAtomProvider } from 'jotai-tanstack-query/react'
    
    const queryClient = new QueryClient()
    
    export const Root = () => {
      return (
        <QueryClientAtomProvider client={queryClient}>
          <App />
          <ReactQueryDevtools />
        </QueryClientAtomProvider>
      )
    }
  4. Incrementally adopt jotai-tanstack-query

    main

    You can use jotai-tanstack-query alongside standard TanStack Query hooks in the same application. The key requirement is to ensure both are using the same QueryClient instance.

    // Standard TanStack/Query
    const { data, isPending, isError } = useQuery({
      queryKey: ['todos'],
      queryFn: fetchTodoList,
    })
    
    // jotai-tanstack-query
    const todosAtom = atomWithQuery(() => ({
      queryKey: ['todos'],
    }))
    
    const { data, isPending, isError } = useAtomValue(todosAtom)
  5. Migrate from v0.7 to v0.8.0

    main

    In v0.8.0, the API was simplified to be more consistent with TanStack Query. The primary changes involve moving from returning tuples of atoms to returning a single atom that contains the full result object.

    1. Atom Signature and Naming

    Functions have been renamed from plural atomsWith... to singular atomWith... because they now return a single atom instead of a tuple.

    2. atomWithQuery and atomWithInfiniteQuery migration

    Previously, these returned [dataAtom, statusAtom]. Now, they return a single atom that provides the full QueryObserverResult<TData, TError>.

    Old way:

    - const [dataAtom, statusAtom] = atomsWithQuery(/* ... */);
    - const [data] = useAtom(dataAtom);
    - const [status] = useAtom(statusAtom);

    New way:

    + const dataAtom = atomWithQuery(/* ... */);
    + const [{ data, isPending, isError }] = useAtom(dataAtom);

    3. atomWithMutation migration

    Previously, atomsWithMutation returned a tuple. Now, atomWithMutation returns a single atom containing the MutationObserverResult and the mutate function.

    Old way:

    - const [, postAtom] = atomsWithMutation(/* ... */);
    - const [post, mutate] = useAtom(postAtom);

    New way:

    + const postAtom = atomWithMutation(/* ... */);
    + const [{ data, error, mutate }] = useAtom(postAtom);
    - const [dataAtom, statusAtom] = atomsWithQuery(/* ... */);
    - const [data] = useAtom(dataAtom);
    - const [status] = useAtom(statusAtom);
    
    + const dataAtom = atomWithQuery(/* ... */);
    + const [{ data, isPending, isError }] = useAtom(dataAtom);
  6. Understand query and mutation result types

    main

    The result of an atom (the value you get when reading the atom) follows specific shapes depending on the atom type:

    Query Results

    • AtomWithQueryResult: The standard result shape (e.g., data, error, status).
    • DefinedAtomWithQueryResult: A result where data is guaranteed to be defined (if applicable).
    • AtomWithSuspenseQueryResult: For suspense queries, this is either the result object or a Promise that resolves to the result object. It omits isPlaceholderData.

    Infinite Query Results

    • AtomWithInfiniteQueryResult: The result shape for infinite queries, including pages and pageParams.
    • DefinedAtomWithInfiniteQueryResult: A result where infinite data is guaranteed to be defined.
    • AtomWithSuspenseInfiniteQueryResult: For suspense infinite queries, this is a Promise resolving to the result object (omitting isPlaceholderData).

    Mutation Results

    • AtomWithMutationResult: Includes the standard mutation observer result plus mutate (void return) and mutateAsync (Promise return) functions.
  7. Handle initialData for queries and infinite queries

    main

    Jotai Query provides specific types to ensure initialData is handled correctly based on whether it is provided or undefined.

    Standard Queries

    • UndefinedInitialDataOptions: Used when initialData is not provided (undefined).
    • DefinedInitialDataOptions: Used when initialData is explicitly provided as a value or a function returning a value. The value must not be undefined.

    Infinite Queries

    • UndefinedInitialDataInfiniteOptions: Used when initialData is not provided for an infinite query.
    • DefinedInitialDataInfiniteOptions: Used when initialData is explicitly provided for an infinite query. The value must be a non-undefined InfiniteData object or a function returning one.
  8. Fix `queryKey` type inference in `atomWithQuery`

    main

    When using atomWithQuery, the queryKey type may default to unknown if the get function is not explicitly typed. To ensure proper type inference for the queryKey (allowing you to access its elements with correct types inside the queryFn), explicitly declare the get parameter with the Getter type from jotai.

    import { Getter } from 'jotai'
    
    // ✅ With explicit Getter type, queryKey gets proper type inference
    const userAtom = atomWithQuery((get: Getter) => ({
      queryKey: ['users', get(idAtom).toString()],
      queryFn: async ({ queryKey: [, id] }) => {
        // typeof id = string
      },
    }))
    import { Getter } from 'jotai'
    
    // ❌ Without explicit Getter type, queryKey type might be unknown
    const userAtom = atomWithQuery((get) => ({
      queryKey: ['users', get(idAtom).toString()],
      queryFn: async ({ queryKey: [, id] }) => {
        // typeof id = unknown
      },
    }))
    
    // ✅ With explicit Getter type, queryKey gets proper type inference
    const userAtom = atomWithQuery((get: Getter) => ({
      queryKey: ['users', get(idAtom).toString()],
      queryFn: async ({ queryKey: [, id] }) => {
        // typeof id = string
      },
    }))
  9. Use atomWithInfiniteQuery

    main

    atomWithInfiniteQuery is used for paginated data (Infinite Queries). It requires getNextPageParam and initialPageParam options, similar to TanStack Query's useInfiniteQuery.

    import { atom, useAtom } from 'jotai'
    import { atomWithInfiniteQuery } from 'jotai-tanstack-query'
    
    const postsAtom = atomWithInfiniteQuery(() => ({
      queryKey: ['posts'],
      queryFn: async ({ pageParam }) => {
        const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}`)
        return res.json()
      },
      getNextPageParam: (lastPage, allPages, lastPageParam) => lastPageParam + 1,
      initialPageParam: 1,
    }))
    
    const Posts = () => {
      const [{ data, fetchNextPage, isPending, isError, isFetching }] =
        useAtom(postsAtom)
    
      if (isPending) return <div>Loading...</div>
      if (isError) return <div>Error</div>
    
      return (
        <>
          {data.pages.map((page, index) => (
            <div key={index}>
              {page.map((post: any) => (
                <div key={post.id}>{post.title}</div>
              ))}
            </div>
          ))}
          <button onClick={() => fetchNextPage()}>Next</button>
        </>
      )
    }
  10. Use atomWithQueries

    main

    atomWithQueries implements TanStack Query's useQueries for dynamic parallel queries. It allows running multiple queries concurrently and optionally combining their results using a combine function.

    Basic usage: Returns an array of query atoms.

    Advanced usage: Uses the combine option to merge results into a single object.

    import { Atom, atom, useAtom } from 'jotai'
    import { type AtomWithQueryResult, atomWithQueries } from 'jotai-tanstack-query'
    
    const userIdsAtom = atom([1, 2, 3])
    
    // Basic usage: Returns an array of query atoms
    const userQueryAtomsAtom = atom((get) => {
      const userIds = get(userIdsAtom)
      return atomWithQueries({
        queries: userIds.map((id) => () => ({
          queryKey: ['user', id],
          queryFn: async ({ queryKey: [, userId] }) => {
            const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
            return res.json()
          },
        })),
      })
    })
    
    // Advanced usage: Combine multiple query results
    const combinedUsersDataAtom = atom((get) => {
      const userIds = get(userIdsAtom)
      return atomWithQueries({
        queries: userIds.map((id) => () => ({
          queryKey: ['user', id],
          queryFn: async ({ queryKey: [, userId] }) => {
            const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
            return res.json()
          },
        })),
        combine: (results) => ({
          data: results.map((result) => result.data),
          isPending: results.some((result) => result.isPending),
          isError: results.some((result) => result.isError),
        }),
      })
    })
  11. Use atomWithMutationState

    main

    atomWithMutationState provides access to all mutations currently in the TanStack Query MutationCache based on a provided filter (like mutationKey).

    const mutationStateAtom = atomWithMutationState((get) => ({
      filters: {
        mutationKey: ['posts'],
      },
    }))