Improper type signature for `dispatch` method despite using "app dispatch" hook - redux

Introduction
I'm using Redux Toolkit to add Redux support to a React application served by a Django app. We're using Typescript, and so we're following the Typescript Quick Start from the Redux Toolkit docs.
An important element of using Redux (Toolkit?) with Typescript is to ensure that your dispatch(...) method has the correct type signature. As described in the docs, if you simply use the useDispatch(...) hook from react-redux,
the default Dispatch type does not know about thunks. In order to correctly dispatch thunks, you need to use the specific customized AppDispatch type from the store that includes the thunk middleware types, and use that with useDispatch. Adding a pre-typed useDispatch hook keeps you from forgetting to import AppDispatch where it's needed.
That's fine, and I followed those instructions. My code exports a new hook, just like the tutorial:
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
My problem is: when I use that hook, the type signature for the dispatch(...) method still doesn't accept thunks. The following code:
const dispatch = useAppDispatch();
dispatch(customAction({ ... params ... }));
Produces a compilation error:
ERROR in [at-loader] ./src/components/Widget.tsx:45:9
TS2345: Argument of type 'AsyncThunkAction<void, CustomActionParams, {}>' is not assignable to parameter of type 'AnyAction'.
Property 'type' is missing in type 'AsyncThunkAction<void, CustomActionParams, {}>' but required in type 'AnyAction'.
My question: Why isn't the typed dispatch method correctly accepting AsyncThunkActions?
Things I've Tried
Interestingly, calling dispatch(...) with the AppDispatch type explicitly parameterized results in the same error. Only parameterizing dispatch(...) with any silences the error, which obviously isn't ideal.
const dispatch = useAppDispatch();
dispatch<AppDispatch>(customAction({ ... params ... })); // same error
dispatch<any>(customAction({ ... params ... })); // error silenced
This makes me think that the issue is AppDispatch not correctly inferring that it should take AsyncThunkAction types, which should happen automatically. Something that could be causing this is that my async action is defined within extraReducers, like so:
export const customAction = createAsyncThunk(
"slice/customAction",
async (payload: CustomActionParams) { ... }
);
export const slice = createSlice({
name: "slice",
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(customAction.fulfilled, (state, action) => { ... });
}
});
I'm not sure why this wouldn't cause the type to update, but it's something I've got to resolve.
Differences from standard setup
A few things about my environment could also theoretically contribute to this issue, so I'm including them here for correctness:
This React application is not a single page application, but is embedded in a rendered Django template by using django-react-templatetags. This library automatically injects script tags inline to pass component props and initialize components using ReactDOM. See this page for an example on how this works.
This means that each component has its own <Provider> component, instead of one <Provider> at the root, but all share the same store.
The useAppDispatch(...) custom method is defined in my store file (store.ts) rather than a hooks.ts at the root, but that shouldn't cause any issues.
Edit: All of this is in a Yarn 2 Workspace, rather than the root directory of a package.
Questions that are not duplicates:
This question uses an incorrect definition for useAppDispatch(...). I'm using the correct definition in my code.
This question doesn't use a custom dispatch hook.
This question uses incorrect and overspecified types for the dispatch method of the thunkAPI object in the payloadCreator method of createAsyncThunk, which I don't currently use. I'm trying to use dispatch as imported via hook in a component, not in my payloadCreator method.

This issue was ultimately caused by some sort of version mismatch between the react-redux and #reduxjs/toolkit packages. I removed my yarn.lock and deleted my node_modules, and reinstalled from scratch, and the issue disappeared!
(Additionally, I also had to pin TypeScript at ~4.1.5 due to a separate issue installing TypeScript with Yarn 2.)

Related

Calling dispatch in useEffect?

At the moment I am simple calling it inside the useEffect hook, without having it as a dependency as I only want it to run once. This however gives React Hook useEffect has a missing dependency: 'dispatch'. Should I add dispatch to the dependencies or is there a better practice for this?
useEffect(() => {
dispatch(handleGetUser());
}, []);
The dispatch function doesn't change between re-renders. The eslint warning isn't aware of what each function does and whether or not it will be recreated or not and hence it shows the warning to prompt the user if he/she has missed something
You can safely disable the warning in this case and not include dispatch as a dependency
useEffect(() => {
dispatch(handleGetUser());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Even if you include dispatch as a dependency you will not see side-effects as dispatch will not change unless the library exposing it has a bug
As #Shubham Khatri said, the dispatch function doesn't change between re-renders.
That means it is safe to add it to the dependency list and it won't make your code re-run:
useEffect(() => {
dispatch(handleGetUser());
}, [dispatch]);
The official React guidance cautions against disabling the lint rule because it can easily lead to subtle bugs. It's better to write your effect in a way that makes the linter happy (and the linked docs give more guidance on how to do so in the general case).

Why are NgRx Action types formatted as an unenforced "[Source] Event" string?

I'm reading NgRx's basic Architecture tutorial, and they explain about Actions:
The interface has a single property, the type, represented as a string. The type property is for describing the action that will be dispatched in your application. The value of the type comes in the form of [Source] Event and is used to provide a context of what category of action it is, and where an action was dispatched from.
https://ngrx.io/guide/store/actions
I'm wondering about the rationale or benefit of not enforcing this kind of arbitrary string format? Why not have Actions enforce you defining both a source and event property that can be composed by the library into a type attribute? It seems like it leaves room for error, making it easy to ignore the recommended format. Is there any advantage to this?
It's just a convention. Strings can be used as types, so it's "safe" from that perspective. At some point, the onus has to be on the developer to choose meaningful strings, right?
I tend to do this to keep things consistent for each suite of actions:
import { createAction, props } from '#ngrx/store';
import { Config } from '../models/config.model';
const appAction = '[App]';
export const getConfig = createAction(
`${appAction} Get Config`
);
export const setConfig = createAction(
`${appAction} Set Config`,
props<{ config: Config }>()
);
Here is a tangentially related article about actions and action creators. Interesting read, but doesn't truly answer your question.

Redux Dev Tools reporting <UNDEFINED> type

I am relatively new to Redux but I'm finding that in a few cases the devtools report an Action type of <UNDEFINED> and yet by printing to console immediately before dispatch (in the action creator) I see that the object does indeed have it's type:
Has anyone else seen this behaviour?
It just means that the action type is not serialized via JSON.stringify. Most likely you're using ES6 Symbol as type. So, JSON.stringify({ type: Symbol('BECOMES_UNDEFINED') }) === '{}'.
If you want Redux DevTools Extension to support unserializable data, set serialize parameter to true:
const store = Redux.createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
serialize: true
}));
It will handle dates, regexes, undefined, error objects, symbols and functions.
I think you've imported the action as an function, try to call (payload) might solve your issue

Component is not unmount after its delete in store

Project (Todolist) was created with immutable library, source here
Store structure: project have many tasks, In redux store: State - map, projects, tasks - Records
When I asyncly remove project ...
export const removeProject = project => (dispatch) => {
if (!isProjectExist(project)) return Promise.resolve()
return projectService
.delete(project)
.then(
() => {
dispatch(remove(project))
console.log("post removeProject resolved")
},
handleError,
)
}
.... that was created after initialization - it will be deleted and properly unmounted, but when project was passed as initialState - ProjectList will not be rerendered, and ProjectItem try to render itself with stale data, and fail, as in picture
It have tests
It looks like reducer returs changed data, but I use immutablejs, and previously i use normalizr-immutable, but I thought that source of issue in this library and write my own normalizeInitialState (source), it did not help, now I think that maybe source of problem in redux-immutable
I struggled entire day on solving of this problem
creator of redux says
I don't think this is something we can fix. React state changes are
asynchronous and React may (or may not) batch them. Therefore, the
moment you press “Remove”, the Redux store updates, and both Item and
App receive the new state. Even if the App state change results in
unmounting of Items, that will happen later than mapStateToProps is
called for Item.
Unless I'm mistaken, there is nothing we can do. You have two options:
Request all required state at App (or a lower, e.g. ItemList) level
and pass it down to “dumb” Items. Add safeguards to mapStateToProps
for “currently unmounting” state. For example, you may return null
from render in this case. Potentially we could have the component
generated by connect() return null from its render if mapStateToProps
returned null. Does this make any sense? Is this too surprising?
Hm, I never saw stubs like return (<div></div>) or safeguards in mapStateToProps in others code
markerikson
I'm not entirely sure I follow what exactly your problem is, but as a
guess: it sounds like the child component is re-rendering before the
parent is. This is a known issue with React-Redux v4 and earlier. The
v5 beta fixes that issue. Try installing react-redux#next and see if
that takes care of your problem.

Isomorphic Redux with code splitting and lazy loaded reducers

I am building an isomorphic application with code splitting using react router and redux. I have gone about as far as I can, but I need some help to figure out the rest of my problem. I have a large application that requires code splitting for the front end. I have a reducer registry that enables me to register new reducers(lazy loaded), or replace existing reducers in my store. This works great, however because sections of my app are lazy loaded, my lazy loaded reducers are not present when I call combineReducers() on the client side, while they resolve perfectly on the server. This causes an unexpected keys error, and forces my store to ignore the offending key(s) in my initial state.
initialState (from server)
{ "cases": {...}, "user": {...} }
Client side redux expected initialState
This is based off of available reducers
{ "user": {...} }
Loaded Reducer
UserReducer
Lazy Loaded Reducer
CaseReducer
The error occurs when I call the following
const finalCreateStore = compose(
applyMiddleware(promiseMiddleware)
)(createStore);
const rootReducer = combineReducers({...reducers})
const store = finalCreateStore(rootReducer, initialState);
Unexpected key "case" found in initialState argument passed to createStore. Expected to find one of the known reducer keys instead: "user". Unexpected keys will be ignored.
Everything works well on the server, but initializing the app on the client while momentarily missing a reducer until it is loaded is causing this error. Does anyone know how to get around this error, or tell redux to not validate the shape of the initial state? I need "cases" to be available to my lazy loaded reducer.
It seems like you should opt not to use the built-in combineReducers, since you know the warning isn't applicable to your usage. From the Redux guide:
These two ways to write a combined reducer are completely equivalent:
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
function reducer(state, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
So you may as well go with the second option.

Resources