The hard parts of React Query
Life without React Query
const Bookmarks = ({ category }) => {
const [data, setData] = useState([])
const [error, setError] = useState()
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true);
fetch(`${endpoint}/${category}`)
.then(res => res.json())
.then(d => {
setData(d)
setLoading(false)
})
.catch(e => setError(e))
}, [category])
if (loading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</div>
))}
</ul>
)
}
Life without React Query
const Bookmarks = ({ category }) => {
const [data, setData] = useState([])
const [error, setError] = useState()
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true);
fetch(`${endpoint}/${category}`)
.then(res => res.json())
.then(d => {
setData(d)
setLoading(false)
})
.catch(e => setError(e))
}, [category])
if (loading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</div>
))}
</ul>
)
}
- Race conditions
- Loading state
- Empty state
- Data & Error are not reset on category change
- Will fire twice in StrictMode
Life without React Query
const Bookmarks = ({ category }) => {
const [data, setData] = useState([])
const [error, setError] = useState()
const [loading, setLoading] = useState(false)
useEffect(() => {
let ignore = false
setIsLoading(true)
fetch(`${endpoint}/${category}`)
.then(res => {
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
})
.then(d => {
if (!ignore) {
setData(d)
setError(undefined)
}
})
.catch(e => {
if (!ignore) {
setError(e)
setData(undefined)
}
})
.finally(() => {
if (!ignore) {
setIsLoading(false)
}
})
return () => {
ignore = true
}
}, [category])
if (loading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</div>
))}
</ul>
)
}
Life without React Query
While making the actual fetch request can be a pretty trivial exercise, making that state available predictably in your application is certainly not.
- React Query docs
Life with(out) React Query
const Bookmarks = ({ category }) => {
const [data, setData] = useState([])
const [error, setError] = useState()
const [loading, setLoading] = useState(false)
useEffect(() => {
let ignore = false
setIsLoading(true)
fetch(`${endpoint}/${category}`)
.then(res => {
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
})
.then(d => {
if (!ignore) {
setData(d)
setError(undefined)
}
})
.catch(e => {
if (!ignore) {
setError(e)
setData(undefined)
}
})
.finally(() => {
if (!ignore) {
setIsLoading(false)
}
})
return () => {
ignore = true
}
}, [category])
if (loading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</div>
))}
</ul>
)
}
import { useQuery } from '@tanstack/react-query';
const Bookmarks = ({ category }) => {
const { isLoading, data, error } = useQuery({
queryKey: ['bookmarks', category],
queryFn: () =>
fetch(`${endpoint}/${category}`).then((res) => {
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
}),
})
if (loading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</div>
))}
</ul>
)
}
Life with(out) React Query
import { useIssues } from './useIssues';
const Bookmarks = ({ category }) => {
const { isLoading, data, error } = useIssues(category)
if (loading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</div>
))}
</ul>
)
}
import { useQuery } from '@tanstack/react-query';
export const useIssues = (category) => {
return useQuery({
queryKey: ['bookmarks', category],
queryFn: ({ signal }) =>
fetch(`${endpoint}/${category}`, { signal }).then((res) => {
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
}),
})
}
- Race conditions
- Loading state
- Empty state
- Data & Error are not reset on category change
- Will fire twice in StrictMode
- Error handling
import { useQuery } from '@tanstack/react-query';
const Bookmarks = ({ category }) => {
const { isLoading, data, error } = useQuery({
queryKey: ['bookmarks', category],
queryFn: () =>
fetch(`${endpoint}/${category}`).then((res) => {
if (!res.ok) {
throw new Error('Failed to fetch')
}
return res.json()
}),
})
if (loading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</div>
))}
</ul>
)
}
Life with(out) React Query
Data Fetching is simple.
What comes afterwards is not.
Enter React Query
Nikola Mitrović
Development Lead & Technical Architect
Vega IT
Novi Sad, Serbia
Tech Speaker
What is React Query actually?
React Query is a data fetching library
Data fetching
useQuery({
queryKey: ['issues'],
queryFn: () => axios.get('/issues').then((response) => response.data),
})
Data fetching
useQuery({
queryKey: ['issues'],
queryFn: () =>
Promise.resolve([
{ id: '1', title: 'A Feature', status: 'closed' },
{ id: '2', title: 'A Nug', status: 'open' },
]),
})
How can I define a baseUrl with RQ?
How can I access response headers with RQ?
How can I make GraphQL requests with RQ?
Data fetching
React Query doesn't care.
Just return a Promise.
What is React Query actually?
React Query is an async state manager, declarative lib that help us with managing async/server state from the BE.
State Managers
function useIssues() {
return useSelector(state => state.issues)
}
function useIssues() {
return useStore(state => state.issues)
}
React Query as State Manager
const useIssues = () =>
useQuery({
queryKey: ['issues'],
queryFn: () => axios.get('/issues').then((response) => response.data)
})
React Query as State Manager
const useIssues = (select) =>
useQuery({
queryKey: ['issues'],
queryFn: () => axios.get('/issues').then((response) => response.data),
select
})
const useIssueCount = () {
return useIssue((issues) => issues.length)
}
React Query as State Manager
export const useIssues = () =>
useQuery({
queryKey: ['issues'],
queryFn: fetchTodos
})
function ComponentOne() {
const { data } = useIssues()
}
function ComponentTwo() {
// ✅ will get exactly the same data as ComponentOne
const { data } = useIssues()
}
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<ComponentOne />
<ComponentTwo />
</QueryClientProvider>
)
}
React Query as State Manager
import { useDispatch } from 'react-redux';
const dispatch = useDispatch()
const { data } = useQuery({
queryKey: ['issues'],
queryFn: () => axios.get('/issues').then((response) => response.data),
})
React.useEffect(() => {
if (data) {
dispatch(setIssues(data))
}
}, [data])
const [issues, setIssues] = React.useState()
useQuery({
queryKey: ['issues'],
queryFn: () => axios.get('/issues').then((response) => response.data),
onSuccess: (data) => setIssues(data)
})
Client v.s. server state
Server state challenges
Caching
Deduping requests
Updating stale data
Background updates
03
02
04
01
Managing memory & GC
05
06
Performance opt.
Server state challenges
Async/server state triggers
- window focus -
- component mount -
- regain network -
- change
refetchOnWindowFocus
refetchOnMount
refetchOnReconnect
QueryKey
Async State triggers
React Query will refetch data on every async state trigger
Async/server state triggers
- window focus -
- component mount -
- regain network -
- change
refetchOnWindowFocus
refetchOnMount
QueryKey
refetchOnReconnect
Smart refetches
React Query will refetch data on every async state trigger only for stale data, and staleTime default is 0
Data synchronization tool
- What is correct stale time?
- Depends on your domain & use case
- React Query provides the means to synchronize our view with the actual data owner - the backend
- As long as data is fresh, it will always come from the cache only
Stale time
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 2 * 60 * 1000 // 2 minutes
}
}
})
useQuery({
queryKey,
queryFn,
staleTime: 5 * 60 * 100 // 5 minutes
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ✅ globally default to 20 seconds
staleTime: 1000 * 20,
},
},
})
// 🚀 everything todo-related will have
// a 1 minute staleTime
queryClient.setQueryDefaults(
todoKeys.all,
{ staleTime: 1000 * 60 }
)
Stale time
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
retryOnMount: false,
retry: 0,
refetchInterval: 0,
}
}
})
Async/server state triggers
- window focus -
- component mount -
- regain network -
- change
refetchOnWindowFocus
refetchOnMount
QueryKey
refetchOnReconnect
Query keys as dependency params
- Cached separately
- Avoid race conditions
- Automatic refetches
- Avoid stale closure problems
const useIssues = (filters) =>
useQuery({
queryKey: ['issues', filters],
queryFn: () => axios
.get('/issues?filters=${filters}')
.then((response) => response.data),
})
Query keys
Query keys
const useIssues = () => {
const filters = useSelector((state) => state.filters)
return useQuery({
queryKey: ['issues', filters],
queryFn: () => axios.get('/issues?filters=${filters}').then((response) => response.data),
})
}
Query keys
Query Key is "just" a string or serializable object
Colocate keys for better reuse
- src
- features
- Profile
- index.tsx
- services.ts
- queries.ts
- keys.ts
- Issues
- index.tsx
- services.ts
- queries.ts
- keys.ts
Use Query Key factories
const IssueKeys = {
all: () => ['issues'] as const,
lists: () => [...issueKeys.all, 'list'] as const,
list: (filters: string) => [...issueKeys.lists(), { filters }] as const,
details: () => [...issueKeys.all, 'detail'] as const,
detail: (id: number) => [...issueKeys.details(), id] as const,
}
export const IssuesService = {
getAllIssues: (filters) => {
return axios.get('/issues?filters=${filters}').then((response) => response.data)
}
}
import IssuesService from './services'
import IssueKeys from './keys'
const useIssues = (filters) => {
// 🚀 get list of issues
return useQuery({
queryKey: IssueKeys.list(filters),
queryFn: IssuesService.getIssues(filters),
})
}
Use Query Key factories
// 🕺 remove everything related
// to the issue feature
queryClient.removeQueries({
queryKey: IssueKeys.all()
})
// 🚀 invalidate all the lists
queryClient.invalidateQueries({
queryKey: IssueKeys.lists()
})
// 🙌 prefetch a single issue
queryClient.prefetchQueries({
queryKey: IssueKeys.detail(id),
queryFn: IssuesService.getIssue(id),
})
// 🙌 fetch a single issue
useQuery({
queryKey: IssueKeys.detail(id),
queryFn: IssuesService.getIssue(id),
})
- cache data correctly
- refetch automatically
- manual invalidation of cache
- unique per query
Query keys
const useIssues = (filters) => {
return useQuery({
queryKey: ['issues', filters],
queryFn: () => axios.get('/issues?filters=${filters}').then((response) => response.data),
})
}
const IssuesList = (filters) => {
const filters = useSelector((state) => state.filters)
const { data, refetch } = useIssues(filters)
const handleFilterChange = () => {
refetch()
}
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</div>
))}
</ul>
)
}
Query keys: Infinite queries
useQuery({
queryKey: ['issues'],
queryFn: fetchIssues,
})
// 🚨 this won't work
useInfiniteQuery({
queryKey: ['issues'],
queryFn: fetchInfiniteIssues,
})
// ✅ choose something else instead
useInfiniteQuery({
queryKey: ['infiniteIssues'],
queryFn: fetchInfiniteIssues,
})
Recap
Async State Manager
Data sync lib
Keys as factories
03
02
01
Resources
Questions
Thank you!
n.mitrovic@vegait.rs
You can find me at
Link to the slides
The hard parts of React Query
By nmitrovic
The hard parts of React Query
- 182