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.

RTK Query

- 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 isLoading, isError).
  • 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 createReducer+createAction).
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).

A redux store with RTKQ API reducer along with regular reducers

- 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.

A complex request that fires multiple calls to the server

- 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).

It’s okay to use async actions alongside onQueryStarted logics if it seems necessary (Check out RTK’s createAsyncThunk).

- 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 POST /users).

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 useEffect).
  • 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 queryFulfilled promise:

catching the error of the promise inside onQueryStarted

- 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:

creating a selector that serves cached data

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 reslect).

- 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.

You can read more about this topic here.

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.

Summary

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!
liad_shiran

--

--

--

A publication by the Nielsen Tel Aviv Engineering team, where we talk about what we do and how we do things

Recommended from Medium

Gopher meet Plasma: A WebAssembly Experiment

Animated clock using React JS Hooks

Help! My website performance sucks!

Let’s Jam

JavaScript Type Details

Comparing packaged sizes between Webpack 4 and 5

Promise.all() — Handle all rejected promises

Creating a Scraper Using Headless Chrome

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Liad Shiran

Liad Shiran

Javascript developer, Writer, Open-source contributor

More from Medium

My Choice of Global State management for React in 2022

Aliases in React, Jest and VSCode

Why you should be doing schema validation in React projects

The beauty of IntersectionObserver API in JS