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