redux-toolkit icon indicating copy to clipboard operation
redux-toolkit copied to clipboard

[getDefaultMiddleware] problem that the type of ReturnType<typeof store.getState> becomes any

Open stwebyy opened this issue 3 years ago • 15 comments
trafficstars

Problem

import { configureStore } from '@reduxjs/toolkit';
import {
  FLUSH,
  PAUSE,
  PERSIST,
  persistReducer,
  persistStore,
  PURGE,
  REGISTER,
  REHYDRATE
} from 'redux-persist';
import storage from 'redux-persist/lib/storage';

const persistConfig = {
  key: 'hoge',
  version: 1,
  storage
};

export const store = configureStore({
  reducer: persistReducer(persistConfig, rootReducer),
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
      }
    })
});

export type RootState = ReturnType<typeof store.getState>;

type RootState = any

No Problem

import { configureStore } from '@reduxjs/toolkit';
import storage from 'redux-persist/lib/storage';

const persistConfig = {
  key: 'hoge',
  version: 1,
  storage
};

export const store = configureStore({
  reducer: persistReducer(persistConfig, rootReducer)
});

export type RootState = ReturnType<typeof store.getState>;

In this case, the type is correctly defined.

Why does passing getDefaultMiddleware make the type definition any?

Please help to fix this problem. Thanks!

stwebyy avatar Dec 15 '21 05:12 stwebyy

In order to fully see what's going on, we really need to see a CodeSandbox or project that shows this happening, so that we can look at the actual behavior (and also see what the TS configuration looks like). Can you add a CodeSandbox or repo that shows this happening?

markerikson avatar Dec 15 '21 06:12 markerikson

@markerikson https://codesandbox.io/s/reduxjs-redux-toolkit-issues-1831-pefw1?file=/src/Store/index.ts

mpash avatar Dec 15 '21 22:12 mpash

Yeah, this does look like a real issue, although partly triggered by persistReducer. Lenz had a suggestion and I'm going to poke at it to see what happens.

markerikson avatar Dec 16 '21 00:12 markerikson

It looks like this isn't specific to Redux-Persist in any way. I can replicate the behavior with:

     const store = configureStore({
        reducer: combineReducers({
          counter: counterReducer,
        }),
        middleware: (getDefaultMiddleware) =>
          getDefaultMiddleware({
          }),
      })

      type RootState = ReturnType<typeof store.getState>
      const { counter } = store.getState()
      // ERROR
      expectNotAny(counter)
      expectExactType<number>(counter)

However, just removing the combineReducers call, or moving it to be const rootReducer = combineReducers(), works fine.

Feels like it might maybe be related to https://github.com/reduxjs/reselect/issues/559#issuecomment-984375279 , where we saw that TS is having trouble reading types off a generic function passed directly as an argument?

markerikson avatar Dec 16 '21 03:12 markerikson

I have another reproduction of this issue in case it's any help for debugging purposes: https://codesandbox.io/s/kind-dew-oulsm?file=/src/index.ts

mvarrieur avatar Dec 22 '21 18:12 mvarrieur

I was about to report exactly this. Using combineReducers to create the root reducer makes all the properties of the RootState become any. As @markerikson suggested, removing combineReducers makes the type correct again, but will be awesome to understand why it happens, and if it is not a good idea to do (configureStore, as long as I know, calls combineReducers anyway) then it should be clearly documented.

Just to make it very clear how I was defining my RootState type, it was like this:

const reducerMap = {
  home: homeReducer,
  common: commonReducer,
  login: loginReducer,
  stats: statsReducer,
};

const rootReducer = (history: typeof History) =>
  combineReducers({
    ...reducerMap,
    router: connectRouter(history),
  });
export default rootReducer;

export type RootState = ReturnType<ReturnType<typeof rootReducer>>;

So maybe it's a problem with combineReducers?

Regards

danielo515 avatar Jan 07 '22 23:01 danielo515

Short answer is, right now we don't know why it happens :)

It seems to be something about the combination of directly calling combineReducers() to produce either the root reducer or a nested reducer, as part of the arguments to configureStore(), while also using middleware: getDefaultMiddleware => getDefaultMiddleware() at the same time.

But it's not clear whether it is:

  • A bug in combineReducers
  • A bug in getDefaultMiddleware()
  • A bug in configureStore
  • Actually a TS issue that is unrelated to the Redux functions themselves

We did see some sorta similar behavior with Reselect and trying to call generic functions to produce arguments to createSelector(), so it could be a TS problem.

Haven't had a chance to investigate further, and given that there's a straightforward workaround this is admittedly low priority for the foreseeable future.

markerikson avatar Jan 08 '22 00:01 markerikson

I think it may not be strictly related to such combination. I can not get rid of the reducers becoming any, and I am not using combineReducers aymore. My createStore looks like this:

import { configureStore } from '@reduxjs/toolkit';
import { routerMiddleware } from 'connected-react-router';
import { useDispatch } from 'react-redux';
import history from './history';
import { reducerMap } from './rootReducer';
import { connectRouter } from 'connected-react-router';
import windowTitle from './windowtitleMiddleware';

const router = routerMiddleware(history);

// NOTE: Do not change middleares delaration pattern since rekit plugins may register middlewares to it.
const middlewares = [router, windowTitle] as const;

const store = configureStore({
  reducer: {
    router: connectRouter(history),
    ...reducerMap,
  },
  middleware: getDefaultMiddleware => getDefaultMiddleware().concat(middlewares),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();

export default store;

and I still get them as any. I think it has to be something related to combineReducers because my reducerMap is properly typed.

danielo515 avatar Jan 08 '22 00:01 danielo515

I forgot to ask: is there any other workaround I can follow? This is almost defeating the purpose of using typescript

danielo515 avatar Jan 08 '22 00:01 danielo515

Like I said earlier, this seems to happen when you do two things together:

  • put a function call on the right-hand side of reducer: (in other words, calling function A and passing its result immediately as part of the field being defined)
  • Also call middleware: getDefaultMiddleware => getDefaultMiddleware()

Your last example is doing exactly that. There's a connectRouter() call being used as an argument to configureStore() , and you are using getDefaultMiddleware().

The workaround, as mentioned earlier, is to not have any function calls on the right-hand side of reducer: .

So, change it to:

const rootReducer = combineReducers({
  ...reducerMap,
    router: connectRouter(history)
})

const store = configureStore({
  reducer: rootReducer, // NO FUNCTION CALLS HERE NOW,
  middleware: gDM => gDM().concat(middlewares)
})

markerikson avatar Jan 08 '22 01:01 markerikson

Hey! I just made a quick test and the problem seems to be on my reducers. I am using an uncommon pattern, it is called rekit.

The thing is, that it separates reducers per feature, in a similar fashion as duck does. The thing is that it has a "global feature" reducer to give you the opportunity to handle cross-feature actions. It looks like this:

import initialState, { State } from './initialState';
import { reducer as setupAppReducer } from './setupApp';
import { reducer as editRunningSessionReducer } from './editRunningSession';

const reducers = [setupAppReducer, editRunningSessionReducer];

export default function reducer(state = initialState, action) {
  let newState: State;
  switch (action.type) {
    // Handle cross-topic actions here
    default:
      newState = state;
      break;
  }
  return reducers.reduce((s, r) => r(s, action), newState);
}

Where is the problem? As soon as any of the reducers on the array of reducers is not properly typed and it is inferred to return any, the entire result of the whole reducer is typed as any. My codebase was previously javascript, and I'm in the middle of a TS conversion, so this is very common. I just updated my smallest "feature-reducer" to be properly typed and the RootState type gets that bit correctly. Your proposed workaround works as it does my previously posted code.

Maybe this is an issue with TS being too sensitive to any type propagation?

danielo515 avatar Jan 08 '22 01:01 danielo515

This may possibly be fixed as a result of the changes in #2001 .

Can someone try out the CSB CI build from either #2001 or #2024 , and see if that works better now?

markerikson avatar Feb 20 '22 17:02 markerikson

@markerikson Today I ran into this issue while using the default CRA with redux-typescript template. My additional middleware was the routerMiddleware from redux-first-history. Issue was, like descibed above, the combineReducers call used in configureStore. Separate rootReducer does work. Automatic call of combineReducers when using a object directly (like described here) also works. Can we update the documentation to mark the problem more clearly?

My package versions

"@reduxjs/toolkit": "^1.8.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@types/jest": "^24.9.1",
"@types/node": "^12.20.47",
"@types/react": "^16.14.24",
"@types/react-dom": "^16.9.14",
"@types/react-redux": "^7.1.23",
"history": "^5.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-router": "^6.2.2",
"react-router-dom": "^6.2.2",
"react-scripts": "5.0.0",
"redux-first-history": "^5.0.8",
"typescript": "~4.1.5"

DJmRek avatar Mar 13 '22 20:03 DJmRek

faced this issue too :( My codesandbox And yes, when I make reducer outside of configureStore - it's working.

EliseyMartynovSynder avatar Mar 29 '22 10:03 EliseyMartynovSynder

I don't have any new solutions for this, and there are known workarounds. Dropping this out of the 1.9 milestone.

markerikson avatar Aug 16 '22 03:08 markerikson

same problem .It took me almost 1 hour to reach this page

amir-khoshbakht avatar Mar 22 '23 15:03 amir-khoshbakht

also take a look at this:

  as typeof authSlice.reducer,
export const store = configureStore({
  reducer: {
    auth: persistReducer(persistConfig, authSlice.reducer) as typeof authSlice.reducer,
    [authApi.reducerPath]: authApi.reducer,
  },
  // devTools: process.env.NODE_ENV !== "production",
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(authApi.middleware),
});

> In order to fully see what's going on, we really need to see a CodeSandbox or project that shows this happening, so that we can look at the actual behavior (and also see what the TS configuration looks like). Can you add a CodeSandbox or repo that shows this happening?

amir-khoshbakht avatar Mar 22 '23 17:03 amir-khoshbakht

import { configureStore } from '@reduxjs/toolkit';
import {
  FLUSH,
  PAUSE,
  PERSIST,
  persistReducer,
  persistStore,
  PURGE,
  REGISTER,
  REHYDRATE
} from 'redux-persist';
import storage from 'redux-persist/lib/storage';

const persistConfig = {
  key: 'root',
  version: 1,
  storage
};

export const store = configureStore({
  reducer: persistReducer(persistConfig, rootReducer),
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
      }
    })
});

I am using this code and it gives me this error:

ERROR
when using a middleware builder function, an array of middleware must be returned at configureStore

Arbaaz234 avatar Jun 15 '23 18:06 Arbaaz234

@Arbaaz234 that seems like a different issue - can you open it separately, ideally with a codesandbox reproduction/replay?

EskiMojo14 avatar Jun 15 '23 19:06 EskiMojo14

Actually I am a newbie with all this and this is from a different project that I am developing. Just getting an error here at configureStore getDefaultMiddleware.

Arbaaz234 avatar Jun 15 '23 19:06 Arbaaz234

we can't help you without seeing your actual code, and like i said this is a separate issue.

at a random guess, it sounds like you could be doing middleware: (getDefaultMiddleware) => { getDefaultMiddleware() } instead of middleware: (getDefaultMiddleware) => getDefaultMiddleware()

EskiMojo14 avatar Jun 15 '23 19:06 EskiMojo14

Yeah that solved but now another issue is

could not find react-redux context value; please ensure the component is wrapped in a <Provider>

even though I have my app component in the provider with the store. Any suggestions would be of great help. Thankyou!

Arbaaz234 avatar Jun 15 '23 19:06 Arbaaz234

please take this to Reactiflux or open another issue/discussion. I will no longer take random guesses as to what your issue is on this completely separate issue.

EskiMojo14 avatar Jun 15 '23 19:06 EskiMojo14