RTK Query | Invalidate cache of an API service from another API service
Asked Answered
D

3

14

I have created multiple RTK Query API services split into multiple files. For this issue I have two services: "Contracts" and "Properties". The contracts service should be able to invalidate the Properties cache when a contract updates, but even after supplying the "Properties" tag to the Contracts service - the cache is not invalidated.

Here is my setup:

Properties:

export const propertyApi = createApi({
    reducerPath: 'propertyApi',
    baseQuery: fetchBaseQuery({ baseUrl: `${API_BASE_URL}/properties` }),
    tagTypes: ['Properties'],
    endpoints: builder => ({
        // many endpoints
    })
})

export const {
    // many hooks
} = propertyApi

Contracts:

export const contractApi = createApi({
    reducerPath: 'contractApi',
    baseQuery: fetchBaseQuery({ baseUrl: `${API_BASE_URL}/contracts` }),
    tagTypes: ['Contracts', 'Properties'],
    endpoints: builder => ({
        // ...
        modifyContract: builder.mutation<Contract, { contract: Partial<ContractDto>, contractId: Contract['id'], propertyId: Property['id'] }>({
            query: ({ contract, contractId }) => {
                return {
                    url: `/${contractId}`,
                    method: 'PATCH',
                    credentials: "include",
                    body: contract
                }
            },
            // to my understanding, this should invalidate the property cache for the property with 'propertyId', but it doesn't seem to work
            invalidatesTags: (_res, _err, { propertyId }) => ['Properties', 'Contracts', { type: 'Properties', id: propertyId }]
        })
    })
})

export const {
    // ...
    useModifyContractMutation
} = contractApi

Store setup:

export const STORE_RESET_ACTION_TYPE = 'RESET_STORE'

const combinedReducer = combineReducers({
    [photoApi.reducerPath]: photoApi.reducer,
    [authApi.reducerPath]: authApi.reducer,
    [propertyApi.reducerPath]: propertyApi.reducer,
    [cronApi.reducerPath]: cronApi.reducer,
    [contractApi.reducerPath]: contractApi.reducer,
    auth: authReducer
})

const rootReducer: Reducer = (state: RootState, action: AnyAction) => {
    if (action.type === STORE_RESET_ACTION_TYPE) {
        state = {} as RootState
    }
    return combinedReducer(state, action)
}

export const store = configureStore({
    reducer: rootReducer,
    middleware: (getDefaultMiddleware) => {
        return getDefaultMiddleware().concat([
            photoApi.middleware,
            authApi.middleware,
            propertyApi.middleware,
            cronApi.middleware,
            contractApi.middleware,
            errorHandlerMiddleware
        ])
    }
})

setupListeners(store.dispatch)

export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
    ReturnType,
    RootState,
    unknown,
    Action<string>
>

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Demoiselle answered 19/9, 2021 at 12:2 Comment(0)
R
24

If those api services have data depending on each other (which is kinda implied by the fact that they should invalidate each other) they should not be multiple api services - they are really just multiple endpoints of one api. We state so at multiple places in the documentation.

Quoting the quickstart tutorial for example:

Typically, you should only have one API slice per base URL that your application needs to communicate with. For example, if your site fetches data from both /api/posts and /api/users, you would have a single API slice with /api/ as the base URL, and separate endpoint definitions for posts and users. This allows you to effectively take advantage of automated re-fetching by defining tag relationships across endpoints.

Instead, if you want to split that one api into multiple files, you can do so - using the code splitting mechanisms described in the documentation.

That also means that you do not have to add a lot of api slices and middlewares in your configureStore call, but just one.

Redhanded answered 19/9, 2021 at 21:25 Comment(1)
I wish I read this post 3-4 months agoParadrop
E
16

There is a specific invalidateTags method for this reason.

import propertyApi from ...;

...

  query: ...
  async onQueryStarted(_, { dispatch, queryFulfilled }) {
    await queryFulfilled;
    dispatch(propertyApi.util.invalidateTags(["Properties"]));
  },

Also, we want to call it after the query is successfully executed, so as an option we could use onQueryStarted lifecycle api callback with queryFulfilled parameter

queryFulfilled returns a promise which will resolve to the data or throw an exception

Evelyn answered 22/5, 2023 at 7:20 Comment(1)
Your answer could be improved with the help of supporting informationSuzannsuzanna
M
3

If you're like me, and don't have a say in how the API is made but still have to invalidate the other API's Tag here's a workaround:

  ...
  query: ...
  onCacheEntryAdded: (args, { dispatch }) => {
    dispatch(otherAPI.util.invalidateTags(["Tag"]))
  }

Down side: it fires before query is resolved so we can't check for success

Martinet answered 15/2, 2023 at 17:24 Comment(1)
That’s why onQueryStarted exist. Could be used for optimistic updates or pessimistic updates. Take a look at the docs. redux-toolkit.js.org/rtk-query/usage/manual-cache-updatesDeanery

© 2022 - 2024 — McMap. All rights reserved.