Developing an asynchronous web app is a complex task, but also a fundamental one in this day and age.
To achieve a goal of creating great web app experience many decisions have to be made:
- When and where should the data be fetched?
- What should trigger a re-fetch?
- What factor decides if data is stale already?
Most of the time a go-to solution is a useEffect hook with a certain request inside the closest component utilizing data. Is it the most efficient and sufficient way to handle remote state?
What if multiple components refer to the same resource? Sadly, over-fetching becomes an issue.
A lot of meta state must be implemented in order for the UI to represent different async data states, like fetching in progress, error, success etc. And this is just a tip of an iceberg.
A solution tailored specifically to handle async state
ReactQuery handles all the above and many more, as stated on its official website it:
- caches data
- dedupes multiple requests for the same data into a single one
- updates “out of date” data in the background
- knows when data is stale
- reflects data updates as quickly as possible and more.
Basic definitions
Server state vs local state
Server State = as a rule of thumb every bit of data originating from outside sources such as database or server. It’s the most recent representation of the source of truth presented on the frontend. So in at it’s core it can be understood as cache.
Local State = every other piece of information that does not rely on outside sources, e.g. dark mode flag, or some local storage variables that manage layout
Managing server data on front end is managing cache.
Main idea behind managing server state with React Query
React Query is based on using a global registry of downloadable resources, represented by query keys.
QueryKeys define what kind of resources are being fetched or mutated and allows tracking freshness of fetched data
globally within the application, upon provided policies, like stale time etc.
Main concepts are data queries, mutations and invalidation.
Queries require unique query key and data fetching method and subscribe to useQuery hook.
/** It's worth mentioning ['todos'] is a dependency array */
const { isLoading, isError, data, error } = useQuery(['todos'], fetchTodoList)
Mutations are typically used to create/update/delete data or perform server side effects. They are associated with useMutation hook.
const mutation = useMutation(newTodo => {
return axios.post('/todos', newTodo)
})
Invalidation is a term in ReactQuery that means signing that stale and forcing a refetch. QueryClient has an invalidateQueries method that allows mark queries as stale and potentially refetch them.
It’s worth to mention a queryClient global state. It comes with a lot of options such as enabled, which can be used for conditional data fetching e.g. requests that rely on each other. Global configuration is great architecture, because it allows controlling every request within application in one place.
Additional benefits of using RQ
React Query handles a lot of side stuff connected with handling async data operations, such as isLoading flags, success, error states, lazy loading, query cancellation, optimistic updates and many more. It solves a lot of common async related problems out of the box and does so in an elegant fashion.