This is an example of issue for RTK Query API pain points and rough spots feedback thread · Issue #3692 · reduxjs/redux-toolkit
So, my scenario is:
- Display current user name, avatar, etc. in many nooks of the app.
- Show some placeholders before profile is loaded.
- Allow user to update profile and show new profile data after this.
I have following endpoints:
GET /profile
that returns current user profile dataPATCH /profile
that updates current user profile data and returns the updated profile, so I don’t need to refetch this
Currently it’s easier still to have a slice for profile.
The profile API declaration looks like this:
const profileApi = api.injectEndpoints({
endpoints: (build) => ({
getProfile: build.query<Profile, undefined>({
query: () => "/profile",
}),
updateProfile: build.mutation<Profile, Partial<Profile>>({
query: (profile) => ({
url: "/profile",
method: "PATCH",
body: profile,
}),
}),
}),
});
And this is my slice that just have some good old initial state:
const initialState: Profile = {
username: "",
avatar: "",
email: "",
};
const profileSlice = createSlice({
name: "profile",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addMatcher(
profileApi.endpoints.getProfile.matchFulfilled,
(state, action) => Object.assign(state, action.payload)
)
.addMatcher(
profileApi.endpoints.updateProfile.matchFulfilled,
(state, action) => Object.assign(state, action.payload)
);
},
});
And this is my simplest selector in the whole app:
const selectProfile: Selector<AppState, Profile> = (state) => state.profile;
If I avoid using a slice, I have to put some custom logic in my selectors and API declarations. And that logic is a bit more complicated than “just update the object with new properties”.
Due to the lack of placeholders, I have to decide on my own whether the data has been loaded in my selectors:
const initialState: Profile = {
username: "",
avatar: "",
email: "",
};
const selectProfile: Selector<AppState, Profile> = createSelector(
[ profileApi.endpoints.getProfile.select(undefined) ],
(queryResult) => queryResult.isSuccess ? queryResult.data : initialState,
);
And, what confuses me even more, to dispatch changes from the API endpoints:
const profileApi = api.injectEndpoints({
endpoints: (build) => ({
getProfile: build.query<Profile, undefined>({
query: () => "/profile",
}),
updateProfile: build.mutation<Profile, Partial<Profile>>({
query: (profile) => ({
url: "/profile",
method: "PATCH",
body: profile,
}),
// Why do I need to subscribe to the start of the query, if I actually need to do some work when it finishes
async onQueryStarted(_, { dispatch, queryFulfilled }) {
const result = await queryFulfilled;
const updateDraft = (draft) => Object.assign(draft, result.data);
await dispatch(profileApi.util.updateQueryData("getProfile", undefined, updateDraft));
},
}),
}),
});
I don’t feel I need to dispatch here. Actually, I do not emit a new event here, I already have an event, because API responded and this is my event. I would like just to reduce my state with an action, where payload is my response.
Maybe I'm a bit of an old-fashioned thunk lover :) I perceive rtk-query as a convenient synchronizer of the server and client state. If simple hooks were enough for me, I would just use react-query or something.
If it were possible to automatically update the query cache based on mutation results, I would code it something like this:
const profileApi = api.injectEndpoints({
endpoints: (build) => ({
// I added UserId param for showing more general example
getProfile: build.query<Profile, UserId>({
query: (userId) => `/profile/${userId}`,
// Declare initial state
placeholderData: initialState,
}),
updateProfile: build.mutation<Profile, Partial<Profile>>({
query: ({ userId, ...profile }) => ({
url: `/profile/${userId}`,
method: "PATCH",
body: profile,
}),
// A method that is executed synchronously after getting a response,
// without juggling any async/await
onSuccess: (result, args, api) => {
// No dispatch, just *apply* the result...
api.util.updateQueryData(
// ... to the state of getProfile request with "userId" argument ...
"getProfile", args.userId,
// ... with this reducer
(state: Profile) => Object.assign(state, result.data)
);
}
}),
}),
});
And the selector would be simple:
const selectProfile: Selector<AppState, Profile> = profileApi.endpoints.getProfile.select(undefined);
This expectations may be based on react-query “Updates from mutation responses” pattern.