React query

This post was published though it is incomplete since it was requested by a friend

Query by default considers cached data as stale. Stale queries are refetched automatically in the background when

Defaults:

  • refetchOnWindowFocus: true
  • refetchOnMount: true
  • refetchOnReconnect: true
  • refetchInterval
  • cacheTime: 5m
  • retry: 3
  • retryDelay
const queryClient = new QueryClient(); // for default configuration

const queryClient = new QueryClient({
   defaultOptions: {
     queries: {
       refetchOnWindowFocus: false,
     },
   },
 })

Queries:

const { 
    isLoading, 
    isError, 
    isSuccess, 
    isIdle, 
    isFetching, // for refreshing
    isPreviousData,
    data, 
    error, 
    status // loading / error / success / idle
} = useQuery(
    'uniqueKey', // used for refetching, caching, and sharing queries
    asyncFunc,
    options);

To know if there is any active fetching:

  const isFetching = useIsFetching()

useQuery('todos', fetchTodos, { 
    // optionals:

    refetchOnWindowFocus: true / false,  
    retry: 3 / true / false / (failureCount, error) => ..., //
    retryDelay: 
       1000 + 300 each time / 
       1000 / 
       (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    keepPreviousData,
    placeholderData : 

        // Static data
        someFakeData

        // Or some dynamic data
        useMemo(() => generateFakeData(), [])

        // Or even data from previous queries
        () => {
            return queryClient
                      .getQueryData('blogPosts')
                      ?.find(d => d.id === blogPostId)
              },
    initialData: initialPrefechedData,
    staleTime: 1000, // the time from the initialData fetch

})

Pagenation

Since triggering the fetch changes the key, normal pagenation will set the state into success/loading.

function Todos() {
   const [page, setPage] = React.useState(0)
 
   const fetchUsers = (page = 0) => fetch('/api/users?page=' + page).then((res) => res.json())
 
   const {
     isLoading,
     isError,
     error,
     data,
     isFetching,
     isPreviousData,
   } = useQuery(['users', page], () => fetchUsers(page), { 
      keepPreviousData : true 
   })
 
   return (
     <div>
       {isLoading ? (
         <div>Loading...</div>
       ) : isError ? (
         <div>Error: {error.message}</div>
       ) : (
         <div>
           {data.projects.map(project => (
             <p key={project.id}>{project.name}</p>
           ))}
         </div>
       )}
       <span>Current Page: {page + 1}</span>
       <button
         onClick={() => setPage(old => Math.max(old - 1, 0))}
         disabled={page === 0}
       >
         Previous Page
       </button>{' '}
       <button
         onClick={() => {
           if (!isPreviousData && data.hasMore) {
             setPage(old => old + 1)
           }
         }}
         // Disable the Next Page button until we know a next page is available
         disabled={isPreviousData || !data?.hasMore}
       >
         Next Page
       </button>
       {isFetching ? <span> Loading...</span> : null}{' '}
     </div>
   )
 }

Infinite Queries

import { useInfiniteQuery } from 'react-query'
 
 function Projects() {
   const fetchProjects = ({ pageParam = 0 }) =>
     fetch('/api/projects?cursor=' + pageParam)
 
   const {
     data,
     error,
     fetchNextPage,
     hasNextPage,
     isFetching,
     isFetchingNextPage,
     status,
     refetch
   } = useInfiniteQuery(
       'projects',    // key
       fetchProjects, // Fetch function
       {
           getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
           getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
           
           // For reverse order
           select: data => ({
             pages: [...data.pages].reverse(),
             pageParams: [...data.pageParams].reverse(),
           });
       });

 
   return status === 'loading' ? (
     <p>Loading...</p>
   ) : status === 'error' ? (
     <p>Error: {error.message}</p>
   ) : (
     <>
       {data.pages.map((group, i) => (
         <React.Fragment key={i}>
           {group.projects.map(project => (
             <p key={project.id}>{project.name}</p>
           ))}
         </React.Fragment>
       ))}
       <div>
         <button
           onClick={() => fetchNextPage()}
           disabled={!hasNextPage || isFetchingNextPage}
         >
           {isFetchingNextPage
             ? 'Loading more...'
             : hasNextPage
             ? 'Load More'
             : 'Nothing more to load'}
         </button>
       </div>
       <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
     </>
   )
 }

When an infinite query becomes stale and needs to be refetched, each group is fetched sequentially starting from the first one

to refetch specific page use

refetch({ refetchPage: (page, index) => index === 0 });
//refetchPage: (page: TData, index: number, allPages: TData[]) => boolean

Pass values to page fetch

const skipToPage50 = () => fetchNextPage({ pageParam: 50 })

Manually edit a specific page:

//Manually removing first page:
queryClient.setQueryData('projects', data => ({
   pages: data.pages.slice(1),
   pageParams: data.pageParams.slice(1),
 }))

//Manually removing a single value from an individual page:
 const newPagesArray = oldPagesArray?.pages.map((page) =>
   page.filter((val) => val.id !== updatedId)
 ) ?? []
 
 queryClient.setQueryData('projects', data => ({
   pages: newPagesArray,
   pageParams: data.pageParams,
 }))

Custom Window Focus Event

focusManager.setEventListener(handleFocus => {
   // Listen to visibilitychange and focus
   if (typeof window !== 'undefined' && window.addEventListener) {
     window.addEventListener('visibilitychange', handleFocus, false)
     window.addEventListener('focus', handleFocus, false)
   }
 
   return () => {
     // Be sure to unsubscribe if a new handler is set
     window.removeEventListener('visibilitychange', handleFocus)
     window.removeEventListener('focus', handleFocus)
   }
 })

Or in react way

 import { AppState } from 'react-native'
 import { focusManager } from 'react-query'
 
 focusManager.setEventListener(handleFocus => {
   const subscription = AppState.addEventListener('change', state => {
     handleFocus(state === 'active')
   })
 
   return () => {
     subscription.remove()
   }
 })


 import { focusManager } from 'react-query'
 
 // Override the default focus state
 focusManager.setFocused(true)
 
 // Fallback to the default focus check
 focusManager.setFocused(undefined)

Ignoring Iframe Focus Events

 import { focusManager } from 'react-query'
 import onWindowFocus from './onWindowFocus' // The gist above
 
 focusManager.setEventListener(onWindowFocus)

Or in react way

Disabling/Pausing Queries or manual execution / serial queries

use the enabled option to tell a query when it is ready to run

 const { data: user } = useQuery(['user', email], getUserByEmail)
 
 const userId = user?.id
 
 const { isIdle, data: projects } = useQuery(
   ['projects', userId],
   getProjectsByUser,
   {
     enabled: !!userId // The query will execute only if userId exists
   }
 )

Keys:

//If your query function depends on a variable, include it in your query key
const result = useQuery(['todos', todoId], () => fetchTodoById(todoId))
 
// equal keys: no matter the order
 useQuery(['todos', { status, page }], ...)
 useQuery(['todos', { page, status }], ...)
 useQuery(['todos', { page, status, other: undefined }], ...)

// not equal keys:
 useQuery(['todos', status, page], ...)
 useQuery(['todos', page, status], ...)
 useQuery(['todos', undefined, page, status], ...)


// An individual todo
 useQuery(['todo', 5], ...)
 // queryKey === ['todo', 5]
 
 // An individual todo in a "preview" format
 useQuery(['todo', 5, { preview: true }], ...)
 // queryKey === ['todo', 5, { preview: true }]
 
 // A list of todos that are "done"
 useQuery(['todos', { type: 'done' }], ...)
 // queryKey === ['todos', { type: 'done' }]

Query functions:

any function that returns a promise, either resolve the data or throw an error.

useQuery(['todos'], fetchAllTodos);
useQuery(['todos', todoId], () => fetchTodoById(todoId));

useQuery(['todos', todoId], async () => {
   const data = await fetchTodoById(todoId)
   return data
 });

useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]));


Must throw an exception for error: 
 const { error } = useQuery(['todos', todoId], async () => {
   if (somethingGoesWrong) {
     throw new Error('Oh no!')
   }
 
   return data
 });

 useQuery(['todos', todoId], async () => {
   const response = await fetch('/todos/' + todoId)
   if (!response.ok) {
     throw new Error('Network response was not ok')
   }
   return response.json()
 })


passing keys
 function Todos({ status, page }) {
   const result = useQuery(['todos', { status, page }], fetchTodoList)
 }
 
 // Access the key, status and page variables in your query function!
 function fetchTodoList({ queryKey }) {
   const [_key, { status, page }] = queryKey
   return new Promise()
 }

Different syntax
 useQuery({
   queryKey: ['todo', 7],
   queryFn: fetchTodo,
   ...config,
 })

Parallel queries:

For predefined number of parallel queries:

 function App () {
   // The following queries will execute in parallel
   const usersQuery = useQuery('users', fetchUsers)
   const teamsQuery = useQuery('teams', fetchTeams)
   const projectsQuery = useQuery('projects', fetchProjects)
   ...
 }

For dynamic amount of parallel queries (since that would violate the rules of hooks)

 function App({ users }) {
   const userQueries = useQueries(
     users.map(user => {
       return {
         queryKey: ['user', user.id],
         queryFn: () => fetchUserById(user.id),
       }
     })
   )
 }

React Query DevTools:

The React Query DevTools are bundled only when process.env.NODE_ENV === 'development'

import { ReactQueryDevtools } from 'react-query/devtools'

const MainApp = () => {
  return (
    <div className="App">
      <QueryClientProvider client={queryClient}>
        <ReactQueryDevtools initialIsOpen={false} />
        <TheApp />
      </QueryClientProvider>
    </div> 
  );
}

Options:
    initialIsOpen true/false: Is open my default
    panelProps: {className, style, ...}: Add props to the panel
    closeButtonProps: {onClick, className, style, ...}: close button props
    toggleButtonProps: {onClick, className, style, ...}: toggle button props
    position: top-left / top-right / bottom-left / bottom-right

Embedded mode:

import { ReactQueryDevtools } from 'react-query/devtools'

const MainApp = () => {
  return (
    <div className="App">
      <QueryClientProvider client={queryClient}>
        <ReactQueryDevtoolsPanel style={styles} className={className} />
        <TheApp />
      </QueryClientProvider>
    </div> 
  );
}

TypeScript:

function useGroups() {
   return useQuery<Group[], Error>('groups', fetchGroups)
 }

Further Reading on TkDodo’s blog