According to the Redux Toolkit, “RTK Query is a powerful data fetching and caching tool. It is designed to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.”
With the help of RTK Query, you can significantly decrease the number of actions, reducers, and side effects.
Right out of the box, there is the possibility to query every N seconds, auto cache, and send repeated requests in the case when cache became invalid.
It’s definitely a cool library however it’s sadly not applicable to all of the use cases possible with an API. This is because it’s managing both the state and all mutations.
For example, I once tried to use it to query some table data and change it -- in other words, basic CRUD operations. I was faced, however, with an issue when I wanted to add additional data to objects and update that data without calling the API.
For example, if I want to handle a few basic requests via RTK Query and not include manage state, or to not write reducers logic, etc.
For better code readability, I realized it’s possible to use the API splitting approach and created baseApi
:
export const baseApi = createApi({
reducerPath: 'baseApi',
tagTypes: ['Notifications'],
baseQuery: fetchBaseQuery({ baseUrl: getBaseUrl() }),
endpoints: () => ({}),
});
For the API, which I need to query, I then wrote notificationsApi
:
const notificationsApi = baseApi.injectEndpoints({
endpoints: build => ({
fetch: build.query<INotification[], INotificationBaseRequest>({
query: buildUrl,
transformResponse: (x: IResponse<INotification[]>) => x.result,
providesTags: ['Notifications'],
}),
remove: build.mutation<void, IRemoveNotificationRequest>({
query: arg => ({
url: buildUrl(arg),
method: 'DELETE',
}),
invalidatesTags: (_: any, id: any) => [{ type: 'Notifications', id }],
}),
removeAll: build.mutation<void, INotificationBaseRequest>({
query: arg => ({
url: buildUrl(arg),
method: 'DELETE',
}),
invalidatesTags: ['Notifications'],
}),
}),
});
export const { useFetchQuery, useRemoveMutation, useRemoveAllMutation } = notificationsApi;
Where buildUrl
is a simple function that combines URL arguments in one string, such as notification ID to ‘baseUrl/notifications/{id}’.
In the endpoints
section I introduced three operations that I needed to use.
The fetch
operation retrieves data by GET request
.
The data looks like an array:
[ { id: number; name: string; message: string; }, ... ]
This array I can easily get in component via the useFecthQuery
hook.
const { data, isLoading } = useFetchQuery({ ...params });
Here, I don’t need to implement isLoading
logic that actually describes the status of the HTTP request.
It is already there in the hook.
However, please be careful, the
isLoading
is for the first query only.
For common casesisFetching
is more appropriate. The hook will automatically request data (in case if there is no data or the cache is invalid). More about cache.
The remove
operation is used to send a DELETE request and remove one notification.
The invalidatesTags
is there to remove only one entry from the array by id.
const [remove] = useRemoveMutation();
Use it without dispatch, just as it is.
You can call remove
on a button click or another user action. As well as the removeAll
operation.
const [removeAll, { isLoading: isDeleting }] = useRemoveAllMutation();
Also in such an operation, there is a possibility to use properties to understand the status of HTTP requests. The main difference between remove
and removeAll
is that in removeAll I remove the whole cache.
That’s pretty much it, all data stored, actions automatically generated. No need to manage it manually… or so I thought.
But I understood that I want to extend objects in the array, to have an additional property: isCollapsed
. It can not be done easily - just changing the current state of cached objects.
The RTK Query manages the state by itself and I can’t create another action to handle such behavior. However, I found two ways how to solve it!
First of all, I could create my own state NotificationState
, where there will be additional data stored, of course, I need to createSlice
or createReducer
and add more actions.
Or I will not use RTK Query, and all will be done via createSlice
where objects will be extended with the required fields.
The general takeaway here is, if for some reason you need to extend objects, or in your API, objects come in different schemas and you want to store it all in one place, it is better to manage the state by yourself from the begining.