redux-toolkit
redux-toolkit copied to clipboard
[getDefaultMiddleware] problem that the type of ReturnType<typeof store.getState> becomes any
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!
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 https://codesandbox.io/s/reduxjs-redux-toolkit-issues-1831-pefw1?file=/src/Store/index.ts
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.
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?
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
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
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.
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.
I forgot to ask: is there any other workaround I can follow? This is almost defeating the purpose of using typescript
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)
})
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?
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 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"
faced this issue too :(
My codesandbox
And yes, when I make reducer outside of configureStore - it's working.
I don't have any new solutions for this, and there are known workarounds. Dropping this out of the 1.9 milestone.
same problem .It took me almost 1 hour to reach this page
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?
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 that seems like a different issue - can you open it separately, ideally with a codesandbox reproduction/replay?
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.
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()
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!
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.