Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save isqua/49d1b6f52351b0ff09db7462f4e7be5c to your computer and use it in GitHub Desktop.
Save isqua/49d1b6f52351b0ff09db7462f4e7be5c to your computer and use it in GitHub Desktop.
Proposal of updating cache data from mutations for redux toolkit query

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:

  1. Display current user name, avatar, etc. in many nooks of the app.
  2. Show some placeholders before profile is loaded.
  3. Allow user to update profile and show new profile data after this.

I have following endpoints:

  • GET /profile that returns current user profile data
  • PATCH /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.

Using a slice

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;

Avoiding to use a slice

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.

An imaginary endpoints with cache update declaration

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment