Isomorphic Redux with code splitting and lazy loaded reducers - redux

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.

Related

NgRx reducer function with condition

I have a side effect that detects the browser language and dispatches a browserLanguageSupported action if it is a language that my application can handle.
Now I have following reducer function that only updates the states preferredLanguage property in case it is not defined already. This is important because there are other actions that update this state property and I do not want a late browserLanguageSupported action to overwrite such a state update.
export interface State {
preferredLanguage: AppLanguage | undefined;
rehydrationComplete: boolean;
}
export const initialState: State = {
preferredLanguage: undefined,
rehydrationComplete: false
};
export const reducer = createReducer(
initialState,
on(LanguageActions.browserLanguageSupported, (state, {browserLanguage}) => {
if (!state.preferredLanguage) {
return {...state, preferredLanguage: browserLanguage};
}
return state;
})
);
Now for my question: Is it good practice to have such a condition in a reducer operator? The function itself is still pure. But I am not sure if it is good design or if I should solve it differently, lets say by adding state slice selection in the side effect that dispatches this action.
Btw. the reason I am not setting it directly in the initial state is because I get the browser language from an angular service and I am not sure if it is even possible to set initial feature state from service injection?
Best regards,
Pascal
I would to this the same way, so you get a đź‘Ť from me.
Adding a slice of the state into the effect just adds needless complexity.
The reducer contains the state, and it's OK to add logic to see if state needs to be updated or not.
Also, let's say you need to add this logic into another action/effect.
Having it in the reducer makes it easier to reuse if it's needed. Otherwise you end up with duplicate logic.
As long as the rejection (or mutation) of the data is irrelevant to the chain of actions & effects, this is absolutely valid.
However, it's worth noting that if the action in question triggers an effect which triggers an action, the triggered action will not know whether the data was rejected (or mutated) without checking the state—which is exactly what this pattern is attempting to avoid.
So, if you wanted to be able react to that rejection (or mutation), you would want to handle this in the effect. But, if you would proceed in exactly the same manner regardless of the result, then it belongs reducer.

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

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.)

In Redux: how to store data from initial html to store? initialState or dispatch?

We have a dillema.
Certain data arrives through a template to our initial index.html page.
We put it on the window and the want to place it in the store.
Right now, we have something like this (psuedoish):
class App ... {
componentDidMount() {
this.props.setLoggedInUser(this.props.user);
// setLoggedInUser is a dispatch prop
}
}
<App user={window.user} />
option two is to just use initial state:
createStore(reducer, { user: window.user }, ...)
We had a lengthy discussion and can't agree if initialState is an anti-pattern or not.
Which is the correct way to implement this type of data loading?
I think initialState is not intended for such use. I would probably initialize it in reducer:
userReducer(state=window.user, action) ...
But that is under assumption that window.user is constant. If it is floating kind of thing, than I would probably go dispatch way, but not in componentDidMount, but in piece of code immediately following createStore
Using initialState when creating the store from bootstrapped data is the preferred way according to the documentation (see #2).
When bootstrapping data this way, you never get in a state where you're waiting for the store to apply the dispatched action. Another bonus is that you don't dispatch an action that isn't in relation to something happening in the UI.

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.

What is the initialState used for in Redux createStore function

All:
When I read the source of Redux, in its createStore:
function createStore(reducer, initialState, enhancer) {
......
var currentState = initialState;
......
dispatch({ type: ActionTypes.INIT });
......
}
When create a new store, it sets currentState to initialState, and call reducer in init dispatch to update the default state value. I wonder: generally the currentStatew will be given a value from reducer, then what is the purpose of that initialState?
Thanks
Normally, just specify the initial state as reducer default argument, and let each reducer manage it for themselves.
However in some cases you want to “hydrate” the state with existing data. For example, you might have saved the whole state tree in a localStorage and want to load it from there on startup, or maybe you are rendering on the server, and want to load the initial state you saved on the server from the HTML.
In this case, the initialState to createStore() is useful because it lets you optionally hydrate some parts of the tree where you have pre-populated data. In this case, it would “win” to the reducer default state which is usually the behavior you would want. The parts of the state tree that exist in initialState would be used as is, and the missing parts would be retrieved from the reducers. This makes it possible to “restore” some parts of the state, but always re-initialize the others.
Hopefully this answer should provide a more in-depth explanation of this technique.

Resources