RTK Query Best Practices
TLDR; In this post, I’ll address the main questions and concerns that fellow developers in my group brought up since we started using RTK Query in our newer apps.
In June 2021, Redux Toolkit introduced a powerful tool for data fetching and caching called RTK Query.
Compared to popular alternatives like React-Query, the main strength of RTK Query is its default integration with the redux store. It generates actions that can be caught by other reducers, thunks, and even Redux DevTools, which is very convenient.
- What is the responsibility of RTK Query? 🤔
RTK Query is responsible for:
- Fetching data and keeping cached data on the client-side.
- Mutating data (create/update/delete) and syncing the changes from the server with the cache.
- Keeping track of requests lifecycle (e.g
- Serving cached data while avoiding duplicate requests for the same data.
- Refetching data when necessary (and syncing with the cache).
- Should we use RTK Query for EVERY request? 🤔
Ideally, yes. We should use the API object (RTK Query exports
createApi) to query/mutate any exposed API endpoint that the app needs. There has to be one API slice per app for RTKQ to work best. For more info, check out: Defining an API slice.
To take full advantage of it, we can, and should, use RTKQ for all requests. Even for requests that are not directly serving UI components (via React hooks). For more info, check out: Usage Without React Hooks.
- Should we still use the good old redux store? 🤔
It’s important to know that
createApi is just an abstraction level over RTK’s
createSlice(which in itself abstracts
It generates another reducer to add to a redux store that may already have other reducers.
There may still be regular reducers in apps that fully leverage RTKQ (non
createApi reducers). They can handle any global state that is not server data. For example, filter state, search term, toggle state, pagination state, etc.
For any server data caching in the client, we should only use RTKQ’s API slice reducer and refrain from holding server data elsewhere (otherwise, we will need to sync its state manually in case of updates/mutations).
- What about complex logic of multiple API calls? 🤔
In most cases, we will need to execute one request followed by another or multiple requests in parallel.
For both of these cases, we should RTKQ’s onQueryStarted- an optional parameter for the endpoint of the first request. It gives us access to the original promise object of the request (
queryFulfilled) and we can either await it or use
Promise.all with it along with other promises.
- Do we still need async actions (e.g. thunks)? 🤔
From my experience with RTKQ, onQueryStarted is very useful and can replace most of the redux async middleware in the application
(e.g. redux-thunk, redux-saga, redux-logic).
With that said, we might still need an async action if we don’t want our logic to depend on a single endpoint, and be more generic (e.g., bootstrapping a page with some requests).
- How to refetch data / invalidate cache properly? 🤔
The main method of refreshing data with RTKQ is using Cache Tags. It’s most helpful when dealing with resources that have both query endpoints and mutation endpoints (e.g.
GET /users and
We can provide a tag to the query endpoint (a simple string tag or a more complex object tag), and all resources from this query endpoint will receive the tag. Once we call a mutation endpoint that is defined to invalidate this tag, all of the query endpoints that have this tag will automatically refetch and update the cache (the store).
In addition to cache tags, there are more methods for cache invalidation (for different use cases). In short:
refetch: A function returned by the query hooks. Triggers a refetch (usually to be called inside
initiate: A function on the endpoint object. Triggers a refetch.
keepUnusedDataFor: How long (seconds) to keep data with no subscribers (no rendered components that use it). The default value is 60.
refetchOnMountOrArgChange: Refetches more frequently - when new components subscribe, or a hook is called with different arguments.
refetchOnFocus: Refetch on window focus or tab switch.
refetchOnReconnect: Refetches if internet connection is re-established.
updateQueryData: A more advanced way to manually update the cache value for an endpoint, for cases when automated updates with Tags aren’t enough (e.g. update before the response arrives).
More information about these can be found here.
- How to handle errors properly? 🤔
The way we receive the error object depends on whether we use the default
fetchBaseQuery (which returns a property called
error) or we use a custom fetch query (in which we can decide how to return the error).
In case we need a special treatment for an error of a specific endpoint, we can do it with
onQueryStarted by catching the error on the
- How to create selectors that use RTKQ’s cached data? 🤔
Each endpoint provides a
select function that returns a selector for its cached data from the store (this selector may receive a cache key as an argument, which is the same argument we would call the query hooks with). We can create a simple selector like so:
Note about the example: in cases where we may need multiple selectors for the same endpoint, it would be best to call
api.endpoints.getUsers.select()separately and create multiple selectors that use it.
This can also be useful for creating composed selectors or selectors that compute data (e.g with
- How to prevent hooks spam in “dumb” components? 🤔
It’s true that RTKQ doesn’t help prevent components from being generic/dumb/presentational. It pushes us to connect more components directly to the store by supplying tons of React hooks.
But, this issue has nothing to do with RTKQ itself.
In my opinion, connecting more components to the store is not a bad practice and also improves performance. This is the direction that many React+Redux apps move toward nowadays in the hooks era, and RTKQ embraces this approach.
If you are just concerned about how quickly we find ourselves using many hooks in one component (that uses multiple kinds of data/mutations), you can easily extract all of the hooks calls to a separate custom hook.
These are the best practices, in my opinion, for the most commonly asked questions I heard from my peers while introducing RTK Query.
I hope I have provided you with some helpful insights on the matter! 😃
Feel free to reach out for anything!