Skip to main content

Query

Server State

  • Tracking loading state in order to show UI spinners.
  • Avoiding duplicate requests for the same data.
  • Optimistic updates to make the UI feel faster
    • Requires asynchronous APIs for fetching and updating.
    • Updating out of date data in the background.
  • Managing cache lifetimes as the user interacts with the UI.
  • RTK Query.
  • React Query.

APIs

  • Query hooks.
  • Mutation hooks.
  • Refetch function.
  • Cache tags.
// Import the RTK Query methods from the React-specific entry point.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// Define our single API slice object.
export const apiSlice = createApi({
// The cache reducer expects to be added at `state.api`.
reducerPath: 'api',
// All of our requests will have URLs starting with '/fakeApi'.
baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
tagTypes: ['Post'],
// The "endpoints" represent operations and requests for this server.
endpoints: builder => ({
getPost: builder.query({
query: postId => `/posts/${postId}`,
}),
// The `getPosts` endpoint is a "query" operation that returns data.
getPosts: builder.query({
// The URL for the request is '/fakeApi/posts'.
query: () => '/posts',
providesTags: ['Post'],
}),
addNewPost: builder.mutation({
query: initialPost => ({
url: '/posts',
method: 'POST',
// Include the entire post object as the body of the request
body: initialPost,
}),
invalidatesTags: ['Post'],
}),
}),
})

// Export the auto-generated hook for the `getPost` query endpoint
export const { useGetPostQuery, useGetPostsQuery, useAddNewPostMutation }
= apiSlice
import { apiSlice } from '../features/api/apiSlice'

export default configureStore({
reducer: {
// ... Other reducers.
[apiSlice.reducerPath]: apiSlice.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(apiSlice.middleware),
})
import { useGetPostsQuery } from '../api'
import { PostExcerpt, Spinner } from '../components'

export function PostsList() {
const {
data: posts = [],
isLoading,
isSuccess,
isError,
error,
refetch,
} = useGetPostsQuery()

const sortedPosts = useMemo(
() => posts.slice().sort((a, b) => b.date.localeCompare(a.date)),
[posts]
)

let content

if (isLoading)
content = <Spinner text="Loading..." />
else if (isSuccess)
content = sortedPosts.map(post => <PostExcerpt key={post.id} post={post} />)
else if (isError)
content = <div>{error.toString()}</div>

return (
<section className="posts-list">
<h2>Posts</h2>
<button type="button" onClick={refetch}>Refetch Posts</button>
{content}
</section>
)
}
import { useState } from 'react'
import { useAddNewPostMutation } from '../api'

export function AddPostForm() {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [userId, setUserId] = useState('')

const [addNewPost, { isLoading }] = useAddNewPostMutation()

const canSave = [title, content, userId].every(Boolean) && !isLoading

const onSavePostClicked = async () => {
if (canSave) {
try {
await addNewPost({ title, content, user: userId }).unwrap()
setTitle('')
setContent('')
setUserId('')
} catch (err) {
console.error('Failed to save the post: ', err)
}
}
}
}

Cache

RTK Query creates a cache key for each unique endpoint + argument combination, and stores the results for each cache key separately.

Use the same query hook multiple times, pass it different query parameters, and each result will be cached separately in Redux store.

It's important to note that the query parameter must be a single value (a primitive value or an object containing multiple fields, same as with createAsyncThunk). RTK Query will do shallow stable comparison of fields, and re-fetch the data if any of them have changed.

By default, unused data is removed from the cache after 60 seconds, can be configured in root API slice definition or overridden in individual endpoint definitions using keepUnusedDataFor flag.

RTK query cache utils:

export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
tagTypes: ['Post'],
endpoints: builder => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: (result = [], error, arg) => [
'Post',
...result.map(({ id }) => ({ type: 'Post', id })),
],
}),
getPost: builder.query({
query: postId => `/posts/${postId}`,
providesTags: (result, error, arg) => [{ type: 'Post', id: arg }],
}),
addNewPost: builder.mutation({
query: initialPost => ({
url: '/posts',
method: 'POST',
body: initialPost,
}),
invalidatesTags: ['Post'],
}),
editPost: builder.mutation({
query: post => ({
url: `posts/${post.id}`,
method: 'PATCH',
body: post,
}),
invalidatesTags: (result, error, arg) => [{ type: 'Post', id: arg.id }],
}),
}),
})
  1. The PATCH /posts/:postId from the editPost mutation.
  2. A GET /posts/:postId as the getPost query is refetched.
  3. A GET /posts as the getPosts query is refetched.

Selector

import {
createEntityAdapter,
createSelector,
createSlice,
} from '@reduxjs/toolkit'
import { apiSlice } from '../api/apiSlice'

const emptyUsers = []

export const selectUsersResult = apiSlice.endpoints.getUsers.select()

export const selectAllUsers = createSelector(
selectUsersResult,
usersResult => usersResult?.data ?? emptyUsers
)

export const selectUserById = createSelector(
selectAllUsers,
(state, userId) => userId,
(users, userId) => users.find(user => user.id === userId)
)

Splitting Endpoints

RTK query code splitting:

  • injectEndpoints(): mutates original API slice object to add additional endpoint definitions and then returns it.
  • enhanceEndpoints(): merged together on a per-definition basis.
  • apiSlice and extendedApiSlice are the same object.
import { apiSlice } from '../api/apiSlice'

export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUsers: builder.query({
query: () => '/users',
}),
}),
})

export const { useGetUsersQuery } = extendedApiSlice

export const selectUsersResult = extendedApiSlice.endpoints.getUsers.select()

Transform Response

import { apiSlice } from '../api/apiSlice'

const usersAdapter = createEntityAdapter()

const initialState = usersAdapter.getInitialState()

export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUsers: builder.query({
query: () => '/users',
transformResponse: (responseData) => {
return usersAdapter.setAll(initialState, responseData)
},
}),
}),
})

export const { useGetUsersQuery } = extendedApiSlice

const selectUsersResult = extendedApiSlice.endpoints.getUsers.select()

const selectUsersData = createSelector(
selectUsersResult,
usersResult => usersResult.data
)

export const { selectAll: selectAllUsers, selectById: selectUserById }
= usersAdapter.getSelectors(state => selectUsersData(state) ?? initialState)

References