Search code examples
reduxredux-toolkitrtk-query

Unable to make a call inside a middleware with RTK-Query


I need, after a user has logged in, make a /me call to get his property.

I solved writing a React Component that returns null:

import { useEffect } from "react";
import { useLazyGetMeQuery } from "../features/apiSlice";

export const Me = () => {
  const [getMe, data] = useLazyGetMeQuery();

  useEffect(() => {
    // If there are no data, trigger it
    /* istanbul ignore next */
    if (!data.data) {
      getMe(undefined, true);
    }
  }, [data]);

  return null;
};

export default Me;

It works. But I don't like the idea to have a Component returning null.

So, I would use a middleware to make this call but cannot.

This is my first try:

import { createListenerMiddleware } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
import { ACTION_SET_ACCESS_TOKEN } from "../utils/constants/constant";
import { apiSlice } from "../features/apiSlice";

const type = `accessTokenSlice/${ACTION_SET_ACCESS_TOKEN}`;
const dispatch = useDispatch();

const meMiddleware = createListenerMiddleware();
meMiddleware.startListening({
  type,
  effect: () => {
    dispatch(apiSlice.util.updateQueryData('getMe', undefined, true));
  }
});

export default meMiddleware;

And this is the second one:

import { createListenerMiddleware } from "@reduxjs/toolkit";
import { ACTION_SET_ACCESS_TOKEN } from "../utils/constants/constant";
import { apiSlice } from "../features/apiSlice";

const type = `accessTokenSlice/${ACTION_SET_ACCESS_TOKEN}`;

const [getMe] = apiSlice.endpoints.getMe.useLazyQuery();

const meMiddleware = createListenerMiddleware();
meMiddleware.startListening({
  type,
  effect: () => {
    getMe(undefined, true);
  }
});

export default meMiddleware;

On both case, I get the warning act.development.js:209 Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. and the error caught TypeError: Cannot read properties of null (reading 'useContext').

The middleware is recalled as

import { configureStore } from "@reduxjs/toolkit";
import { accessTokenSlice } from "../features/accessTokenSlice";
import { userSlice } from "../features/userSlice";
import addAccessTokenMiddleware from "../middlewares/addAccessTokenMiddleware";
import removeAccessTokenMiddleware from "../middlewares/removeAccessTokenMiddleware";
import meMiddleware from "../middlewares/meMiddleware";
import { apiSlice } from "../features/apiSlice";

const setupStore = (preloadedState) =>
  configureStore({
    reducer: {
      [apiSlice.reducerPath]: apiSlice.reducer,
      accessTokenSlice: accessTokenSlice.reducer,
      userSlice: userSlice.reducer
    },
    preloadedState,
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        immutableCheck: false,
        serializableCheck: false
      })
        .prepend(addAccessTokenMiddleware.middleware)
        .prepend(removeAccessTokenMiddleware.middleware)
        .concat(apiSlice.middleware)
        .concat(meMiddleware.middleware)
  });

export default setupStore;

Other than middleware (if is it possible) or a "null" React component, is there some other manner to do a single call - without need of refresh data -, e.g. after an user has logged in, but using RTK Query?


Solution

  • According to official doc,

    RTK Query is a powerful data fetching and caching tool

    It means you don't need to cary about caching data from your backend, RTKq will cache it and avoid unnecessary queries. Just use useQuery hook in component, where you need user property:

    const {
        data: apiResponse,
        isFetching,
        isLoading,
    } = useGetMeQuery();
    

    set rules for cache update (if you need) by options param and tags. RTKq will not execute new query to backend still it have actual cache.

    If you need to save the cache state until the user exits the application and rehydrate it when return (in other words, don't make a new http request after, for example, refreshing browser tab), see redux-persist.

    UPDATED: If you still need to fill cache in a middleware, try this:

    const meMiddleware = createListenerMiddleware();
    meMiddleware.startListening({
        type,
        effect: async (action, listenerApi) => {
            listenerApi.cancelActiveListeners();
            listenerApi.dispatch(
                apiSlice.endpoints.getMe.initiate([arguments]), //pass args to your endpoint
            );
        },
    });
    

    Working example from my project.