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 datedata 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 }],
}),
}),
})
- The
PATCH /posts/:postIdfrom the editPost mutation. - A
GET /posts/:postIdas the getPost query is refetched. - A
GET /postsas 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
injectEndpoints(): mutates original API slice object to add additional endpoint definitions and then returns it.enhanceEndpoints(): merged together on a per-definition basis.apiSliceandextendedApiSliceare 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
- RTK Query real world example.