DEV Community

Nilupul Perera
Nilupul Perera

Posted on

Boost Your Application's Performance with Redux RTK Query 🚀

Cover

Recently, I had the opportunity to work extensively with Redux RTK Query, and let me tell you—it was a game-changer for managing data fetching and caching in my React application. If you’re not using RTK Query yet, you’re missing out on a powerful tool to simplify your code and supercharge your app's performance.

In this post, I’ll share my experience, highlight some practical examples from this repository, and explain when to use queries and mutations effectively.


Why RTK Query?

RTK Query, part of Redux Toolkit, streamlines the process of:

  • Fetching data from APIs.
  • Caching and synchronizing server state.
  • Handling optimistic updates for a snappy UI experience.

When I integrated RTK Query, the automatic caching mechanism significantly reduced redundant API calls. This not only improved application performance but also simplified state management.

RTK-Query Logo

Key Features

  1. Automatic Caching: Say goodbye to manual caching logic.
  2. Reduced Boilerplate: No need to write separate actions or reducers.
  3. Optimistic Updates: Immediate UI updates while waiting for server confirmation.
  4. Efficient Refetching: Invalidate cache selectively to keep data fresh.

When to Use Queries and Mutations

Queries

Use queries for reading or fetching data. For example:

  • Displaying a list of users, posts, or products.
  • Fetching details of a single entity.

Here's a query example to fetch posts:

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

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8080/' }), // base URL of your backend service
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
    }),
  }),
});

export const { useGetPostsQuery } = api;
Enter fullscreen mode Exit fullscreen mode

Using the hook in a component:

const Posts = () => {
  const { data: posts, isLoading } = useGetPostsQuery();

  if (isLoading) return <p>Loading...</p>;

  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Mutations

Use mutations for creating, updating, or deleting data. For example:

  • Adding a new user.
  • Updating product details.
  • Deleting a post.

Here's a mutation example to add a new post:

export const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8080/' }), // base URL of your backend service
  endpoints: (builder) => ({
    addPost: builder.mutation({
      query: (newPost) => ({
        url: '/posts',
        method: 'POST',
        body: newPost,
      }),
    }),
  }),
});

export const { useAddPostMutation } = api;
Enter fullscreen mode Exit fullscreen mode

Using the mutation in a component:

const AddPost = () => {
  const [addPost] = useAddPostMutation();

  const handleAddPost = async () => {
    await addPost({ title: 'New Post', content: 'This is a new post.' });
  };

  return <button onClick={handleAddPost}>Add Post</button>;
};
Enter fullscreen mode Exit fullscreen mode

Caching in Action

The built-in caching mechanism was a lifesaver in my recent project. For example, when a user added a new post, the cache updated automatically, avoiding the need for a full refetch of all posts.

Here’s how RTK Query helps with cache invalidation:

endpoints: (builder) => ({
  addPost: builder.mutation({
    query: (newPost) => ({
      url: '/posts',
      method: 'POST',
      body: newPost,
    }),
    invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
  }),
  getPosts: builder.query({
    query: () => '/posts',
    providesTags: [{ type: 'Posts', id: 'LIST' }],
  }),
});
Enter fullscreen mode Exit fullscreen mode

With this setup, adding a new post automatically triggers a refetch of the posts list, ensuring the UI stays up-to-date.


Performance Gains

After integrating RTK Query:

  1. API calls reduced by 40% thanks to caching.
  2. Component re-renders minimized, improving responsiveness.
  3. Developer experience improved due to simpler and cleaner code.

Final Thoughts

If you're building modern React applications, Redux RTK Query is a must-have tool. Its caching, efficient data fetching, and automatic state management make it a powerful ally in creating high-performance apps.

💡 Curious to learn more? Check out this repository for a hands-on example.

Let me know your thoughts or share your experience with RTK Query! 🚀

Top comments (2)

Collapse
 
starkraving profile image
Mike Ritchie • Edited

I’ve been using RTK at work for a while now, and I love it for one more thing that it makes super easy: mocking the API.

A workflow I really like to use is to get the entire frontend up and working, and signed off by stakeholders, before starting on the backend. The main reason is efficiency: it’s easier to make changes to the data if you don’t have to change it on the backend as well, and it’s easier to write the backend once you’ve got a complete understanding of exactly what it’ll require. As an added bonus, getting the frontend signed off by stakeholders right away means you make them happy by showing them progress quickly, and they get to have meaningful feedback at the beginning of the process, where it makes the most impact.

To facilitate this, I start my queries and mutations by using queryFn instead of query. With that, I can do something like this:

list: builder.query({
  queryFn() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(fixtures.posts);
      }, 2000);
    });
  },
  providesTags: [‘Posts’]
})
Enter fullscreen mode Exit fullscreen mode

I return a Promise to make sure queryFn acts the same way query does, and I use setTimeout with an obvious delay so that I remember to add pending states to the UI. The great thing is that the rest of the application is completely unaware that this is mock data, so everything else is 100% production code. If I need to make changes to the data, I can just change the JSON in the fixture file. And when I’m ready to start on the API endpoint in the backend, I can use the structure in the fixture file as documentation for the data contract.

Once the API is ready, I literally just need to swap the queryFn block with the live query equivalent, and that’s it. It’s a great workflow!

Collapse
 
hasunnilupul profile image
Nilupul Perera

That’s really cool! I’ve also used queryFn to work with Firestore, and it’s been super handy for CRUD operations. It’s perfect for integrating Firestore’s API, whether you’re fetching documents, writing data, or even handling advanced features like real-time updates and complex queries.

What I love about using queryFn is how it keeps the database logic separate from the rest of the app, making everything else so much cleaner and easier to maintain. Plus, Firestore’s features like batched writes or live snapshots work seamlessly with this setup.

If you’ve got any tips or favorite ways you’ve used queryFn for Firestore, let’s swap ideas! 🚀