DEV Community

Cover image for Like react-query and redux? You'll love RTK Query
Fernando González Tostado
Fernando González Tostado

Posted on • Edited on

Like react-query and redux? You'll love RTK Query

React-query —now TanStack Query— is a library that helps you to fetch and manage data in your application. It's perfect for caching and handling data between components and its hooks make your code easy to read and understand.

However, there may be the case where your app uses —or will use— Redux, which IMO is the best option for handling complex state management.

Actually with Redux, you can also fetch and store data in the state using thunks and specifically with ReduxToolKit with createAsyncThunk and handle the thunk promise lifecycle actions in your slice.

Now, supposing that you already have RTK in your app and createAsyncThunk sounds like a lot of boilerplate and you also want a more simpler declarative react-query-type strategy without having to also add react-query in the app?

This is when RTK Query comes into play!

In TanStack Query you would usually fetch declaratively like so:

function App() {
const { isLoading, error, data, isFetching } = useQuery(["repoData"], () =>
 axios
   .get("https://pokeapi.co/api/v2/pokemon/bulbasaur")
   .then((res) => res.data)
 );
Enter fullscreen mode Exit fullscreen mode

We love this declarative way of fetching and caching data!

The great news here is that you can make use of a similar approach with RTK Query.

export default function App() {
 const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
Enter fullscreen mode Exit fullscreen mode

But where is the inline function and the tag? Well, for that we'll use one single api where tags, requests, invalidations are handled. Why? From the authors:

"Our perspective is that it's much easier to keep track of how requests, cache invalidation, and general app configuration behave when they're all in one central location in comparison to having X number of custom hooks in different files throughout your application."

The aforementhioned api should look like this:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// services/pokemon.ts
export const pokemonApi = createApi({
 baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
 tagTypes: [],
 endpoints: (builder) => ({
   getPokemonByName: builder.query({
     // name is the actual pokemon name - we used Bulbasaur in the example
     query: (name: string) => `pokemon/${name}`,
   }),
   getPokemonById: builder.query({
     query: (id: number) => `pokemon/${id}`
   }),
   // add more queries in the api
 }),
})

// Export hooks for usage in functional components
export const { useGetPokemonByNameQuery, useGetPokemonByIdQuery } = pokemonApi
Enter fullscreen mode Exit fullscreen mode

As you may have noted, your queries are handled in your api and the hooks are also exported from it. Leaving you to choose where you want to import and use the hook.

I've added a getPokemonById query to the endpoints object to show you that here's where you are going to declare all your queries.

In my opinion this is great, it makes the hooks less verbose and your component code DRYer while still using the pattern that we love from TanStack Query.
As usual in Redux, with great power comes great responsibility which means some initial boilerplate. Just a bit.

You'll only have to add the middleware in your configureStore method and add an entry for the api in your rootReducer:

import { pokemonApi } from './services/pokemon'

const rootReducer = combineReducers({
 [pokemonApi.reducerPath]: pokemonApi.reducer,
})

export const setupStore = (preloadedState?: PreloadedState<RootState>) => {
 return configureStore({
   reducer: rootReducer,
   middleware: (getDefaultMiddleware) =>
     // adding the api middleware enables caching, invalidation, polling and other features of `rtk-query`
     getDefaultMiddleware().concat(pokemonApi.middleware),
   preloadedState,
 })
}
Enter fullscreen mode Exit fullscreen mode

Nothing changes in the way you wrap your app with RTK. So this should be done already

import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'

import App from './App'
import { setupStore } from './store'

const store = setupStore()

const reactRoot = ReactDOM.createRoot(
 document.getElementById('root') as HTMLElement
)
reactRoot.render(
 <Provider store={store}>
   <App />
 </Provider>)
Enter fullscreen mode Exit fullscreen mode

Finally, the result will be the exactly same with both strategies:

rtk vs rq

You can check out both implementations in here.

Bonus - Data Invalidation

And the data invalidation? You've probably used a mutation with useMutation —or even with useQuery— and you want the data to be invalidated after this request and thus the cache removed and the data refetched.

Supposing that we are fetching several posts from the DB. We'd do it this way

export const api = createApi({
 baseQuery: fetchBaseQuery({ baseUrl: '/' }),
 tagTypes: ['Posts'],
 endpoints: (build) => ({
   getPosts: build.query<Post[], void>({
     query: () => 'posts',
     providesTags: ['Posts'],
   }),
   addPost: build.mutation<Post, Partial<Post>>({
     query: (body) => ({
       url: `post`,
       method: 'POST',
       body,
     }),
     invalidatesTags: ['Posts'],
   }),
Enter fullscreen mode Exit fullscreen mode
  1. Adding the tagTypes key with an array of the request tags, that later will be linked with our requests to create a relationship.
  2. Then a providesTags array that accepts a string or a callback. We'll use a string for the moment which would invalidate the entire request.

And in the component where we invoke the mutation:

 // in YourForm.tsx
   const [addPost] = useAddPostMutation()

   const handleAddPost (body) => {
     addPost(body);
   }

   return (
     <form onSubmit={handleAddPost}>
       // your form
     </form>
   )
Enter fullscreen mode Exit fullscreen mode

Once your mutation request has succeeded any other request "linked" with the providesTag: ['Posts'] will have its cache invalidated, hence the data won't be served immediately but it would be refetched to have the data updated.

You can even avoid refetching the entire request by using the providesTag callback similar to how it's done in TanStack Query. If you are interested in a more advanced usage, the docs are very well documented.

Conclusion

Redux might not be the optimal solution for all the apps, most of the time react-query, Zustand, Jotai or —god forbid— even Context are enough for handling and storing states, however, for those who enjoy —or need to use— Redux will not be disappointed with the huge array of tools that RTK provides to the developers to make their experience with Redux frictionless and in my case, enjoyable.

Foto from Oskar Kadaksoo in Unsplash

Top comments (0)