References

RTK Query Guide

Data fetching and caching with RTK Query, server state management

Core Patterns

  • When to Read This
  • createApi Setup
  • Queries (Read Operations)
  • Mutations (Write Operations)

When to Read This

  • Implementing data fetching with RTK Query
  • Creating API slices with queries and mutations
  • Managing cache invalidation with tags
  • Optimistic updates for better UX
  • Authentication and custom base queries

createApi Setup

✅ Basic API Definition

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

interface Post {
  id: number;
  title: string;
  content: string;
}

export const api = createApi({
  reducerPath: "api",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  tagTypes: ["Post", "User"],
  endpoints: (builder) => ({
    // Endpoints defined here
  }),
});

export const { useGetPostsQuery, useAddPostMutation } = api;

✅ Store Integration

import { configureStore } from "@reduxjs/toolkit";
import { api } from "./api";

export const store = configureStore({
  reducer: {
    [api.reducerPath]: api.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(api.middleware),
});

Queries (Read Operations)

✅ Basic Query

getPosts: builder.query<Post[], void>({
  query: () => "/posts",
  providesTags: ["Post"],
});

✅ Query with Parameters

getPostById: builder.query<Post, number>({
  query: (id) => `/posts/${id}`,
  providesTags: (result, error, id) => [{ type: "Post", id }],
});

✅ Transform Response

getPosts: builder.query<Post[], void>({
  query: () => "/posts",
  transformResponse: (response: { data: Post[] }) => response.data,
  providesTags: ["Post"],
});

✅ Using Queries in Components

function PostsList() {
  const { data, isLoading, error, refetch } = useGetPostsQuery();

  if (isLoading) return <Spinner />;
  if (error) return <Error error={error} />;

  return (
    <div>
      {data?.map(post => <PostItem key={post.id} post={post} />)}
      <button onClick={refetch}>Refresh</button>
    </div>
  );
}

✅ Conditional Fetching

// Skip query if id is undefined
const { data } = useGetPostByIdQuery(id, {
  skip: !id,
});

✅ Polling

const { data } = useGetPostsQuery(undefined, {
  pollingInterval: 5000, // Poll every 5 seconds
});

Mutations (Write Operations)

✅ Basic Mutation

addPost: builder.mutation<Post, Partial<Post>>({
  query: (body) => ({
    url: "/posts",
    method: "POST",
    body,
  }),
  invalidatesTags: ["Post"],
});

✅ Update Mutation

updatePost: builder.mutation<Post, { id: number; data: Partial<Post> }>({
  query: ({ id, data }) => ({
    url: `/posts/${id}`,
    method: "PATCH",
    body: data,
  }),
  invalidatesTags: (result, error, { id }) => [{ type: "Post", id }],
});

✅ Delete Mutation

deletePost: builder.mutation<void, number>({
  query: (id) => ({
    url: `/posts/${id}`,
    method: "DELETE",
  }),
  invalidatesTags: (result, error, id) => [{ type: "Post", id }],
});

✅ Using Mutations

function AddPostForm() {
  const [addPost, { isLoading, error }] = useAddPostMutation();

  const handleSubmit = async (data: Partial<Post>) => {
    try {
      await addPost(data).unwrap();
      // Success
    } catch (err) {
      // Error handling
    }
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

Tag-Based Cache Invalidation

✅ Tag Types

export const api = createApi({
  tagTypes: ["Post", "User", "Comment"],
  // ...
});

✅ Provide Tags (Queries)

// Provide list-level tag
getPosts: builder.query<Post[], void>({
  query: () => "/posts",
  providesTags: ["Post"],
});

// Provide item-level tags + list tag
getPosts: builder.query<Post[], void>({
  query: () => "/posts",
  providesTags: (result) =>
    result
      ? [
          ...result.map(({ id }) => ({ type: "Post" as const, id })),
          { type: "Post", id: "LIST" },
        ]
      : [{ type: "Post", id: "LIST" }],
});

✅ Invalidate Tags (Mutations)

// Invalidate all posts
addPost: builder.mutation({
  // ...
  invalidatesTags: ["Post"],
});

// Invalidate specific post
updatePost: builder.mutation({
  // ...
  invalidatesTags: (result, error, { id }) => [{ type: "Post", id }],
});

// Invalidate list only
addPost: builder.mutation({
  // ...
  invalidatesTags: [{ type: "Post", id: "LIST" }],
});

Optimistic Updates

✅ Manual Cache Update

updatePost: builder.mutation<Post, { id: number; data: Partial<Post> }>({
  query: ({ id, data }) => ({
    url: `/posts/${id}`,
    method: "PATCH",
    body: data,
  }),
  async onQueryStarted({ id, data }, { dispatch, queryFulfilled }) {
    // Optimistic update
    const patchResult = dispatch(
      api.util.updateQueryData("getPosts", undefined, (draft) => {
        const post = draft.find((p) => p.id === id);
        if (post) {
          Object.assign(post, data);
        }
      }),
    );

    try {
      await queryFulfilled;
    } catch {
      patchResult.undo(); // Rollback on error
    }
  },
});

Authentication

✅ Prepare Headers

const baseQuery = fetchBaseQuery({
  baseUrl: "/api",
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).auth.token;
    if (token) {
      headers.set("Authorization", `Bearer ${token}`);
    }
    return headers;
  },
});

✅ Retry on Token Refresh

const baseQueryWithReauth: BaseQueryFn = async (args, api, extraOptions) => {
  let result = await baseQuery(args, api, extraOptions);

  if (result.error?.status === 401) {
    // Try to refresh token
    const refreshResult = await baseQuery("/auth/refresh", api, extraOptions);

    if (refreshResult.data) {
      // Store new token
      api.dispatch(setToken(refreshResult.data));
      // Retry original query
      result = await baseQuery(args, api, extraOptions);
    } else {
      api.dispatch(logout());
    }
  }

  return result;
};

Advanced Patterns

✅ Prefetching

// Prefetch on hover
<Link
  onMouseEnter={() => dispatch(api.util.prefetch('getPostById', postId))}
  to={`/posts/${postId}`}
>
  View Post
</Link>

✅ Manual Cache Subscription

useEffect(() => {
  const promise = dispatch(
    api.endpoints.getPosts.initiate(undefined, {
      subscribe: true,
      forceRefetch: true,
    }),
  );

  return () => {
    promise.unsubscribe();
  };
}, [dispatch]);

✅ SSR / SSG

// Next.js getServerSideProps
export const getServerSideProps = wrapper.getServerSideProps(
  (store) => async () => {
    store.dispatch(api.endpoints.getPosts.initiate());

    await Promise.all(store.dispatch(api.util.getRunningQueriesThunk()));

    return {
      props: {},
    };
  },
);

Error Handling

✅ Custom Error Handling

const { data, error } = useGetPostsQuery();

if (error) {
  if ('status' in error) {
    // FetchBaseQueryError
    const errMsg = 'error' in error ? error.error : JSON.stringify(error.data);
    return <div>Error: {errMsg}</div>;
  } else {
    // SerializedError
    return <div>Error: {error.message}</div>;
  }
}

References