I followed flow docs and typed redux action creators using union (https://flow.org/en/docs/react/redux/#toc-typing-redux-actions)
so I have a file with ALL the actions gathered into 1 union like in example:
type Action =
| { type: "FOO", foo: number }
| { type: "BAR", bar: boolean }
| { type: "BAZ", baz: string };
Action type is imported in my reducers and used as in exxample from docs:
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FOO": return { ...state, value: action.foo };
case "BAR": return { ...state, value: action.bar };
default:
(action: empty);
return state;
}
}
The problem:
As I mentioned I gathered ALL the actions in one file - currently ~600 actions in one union. I noticed that lately flow server takes crazy time to start (100+ seconds), rechecking flow is also a pain if change is related to reducer. According to flow logs, files that contain reducers are marked as "Slow MERGE" - 15 to 45s.
After experimenting, I noticed that changing my Action type to any cuts the time from 100s to 9s.
The question:
can this be related to huge Action union?
should I split it into a few smaller types which will contain only actions to import in particular reducer or this is a wrong way to fix my issue?
It's probably more likely that this one action type is used across your entire app. Any time you make a change to it, Flow needs to recheck a very large number of files. One way to help mitigate this is to ensure all your union actions are in files of their own that don't import other files. Flow can get slow if it has "cycles". One type imports another time which then imports the first time. This can happen if, for example, you define your reducer actions in the reducers themselves. This causes a cycle. Instead, move your action types to their own file.
Additionally, you can use flow cycle to output a dot file you can then visualize this file in something like Gephi https://gephi.org/ to detect cycles.
Related
I have some normalised data (items) within my redux store:
{
items: {
index: ['a','b'],
dict: {
a: {
title: "red",
},
b: {
title: "car",
}
}
},
...
}
So, if I want to update anything within an item object, the reducer looks like this:
...
const itemsReducer = (state = initialState.items, action) => {
switch (action.type) {
case itemsActions.types.UPDATE_ITEM: {
return {
...state,
[action.payload.itemId]: {
title: action.payload.title,
}
}
}
default: return state;
}
};
But this technique creates a new object for items, which can cause unnecessary components to re-render, when really it should only cause components that subscribe to state changes of the individual object to re-render.
Is there any way to get around this?
That is how immutable updates are required to work - you must create copies of every level of nesting that needs to be updated.
In general, components should extract the smallest amount of data that they need from the store, to help minimize the chance of unnecessary re-renders. For example, most of the time a component probably shouldn't be reading the entire state.items slice.
FWIW, it looks like you're hand-writing your reducer logic. You should be using our official Redux Toolkit package to write your Redux logic in general. RTK also specifically has a createEntityAdapter API that will do most typical normalized state updates for you, so you don't have to write reducer logic by hand.
I'll also note that the recently released Reselect 4.1 version has new options you can use for customizing memoized selectors as well.
In our Product we use Angular 6 together with NgRX 6. Instead of defining our constants as export const strings, we use an object to encapsulate them:
export const ACTION_CONSTANTS = {
'OPEN_MODAL' : 'OPEN_MODAL',
'CLOSE_MODAL' : 'CLOSE_MODAL',
'OPEN_TOOLTIP' : 'OPEN_TOOLTIP',
'CLOSE_TOOLTIP' : 'CLOSE_TOOLTIP',
...
};
As the ACTION_CONSTANTS object gets bigger and prefixes get longer ('DROPDOWN_ACTION_SKIP_SET_INIT_STATE'), I would prefer to nest constants e.g. by feature:
export const ACTION_CONSTANTS = {
'MODAL' : {
'OPEN' : 'MODAL.OPEN',
'CLOSE' : 'MODAL.CLOSE'
},
'TOOLTIP' : {
'OPEN' : 'TOOLTIP.OPEN',
'CLOSE' : 'TOOLTIP.CLOSE'
},
...
};
Is it a good idea or are there any downsides? I could not find anything on formatting constants on the Redux FAQ.
I don't think it is a bad idea, as long as you're able to keep it all organized. But I would suggest grouping your actions into different files. I find this the best way to keep things organized.
--ActionsFile
-modalActions.js
-toolTipAction.js
I usually keep actions in different files, roughly aligned with models & reducers. And i have a naming convention like:
ACTION_MODEL_OUTCOME
So, for example, to load model of type ProductGroup i would have actions:
export const ActionTypes = {
LOAD_PRODUCTGROUP: enforceUnique("[ProductGroup] Laod ProductGroup"),
LOAD_PRODUCTGROUP_SUCCESS: enforceUnique("[ProductGroup] Load ProductGroup Success")
LOAD_PRODUCTGROUP_FAILURE: enforceUnique("[ProductGroup] Load ProductGroup Failure")
}
enforceUnique is a function that caches all registered actions and make sure there are no duplicates across the whole app.
Now, when you import actions for certain model, you import only those from file you need (e.g. import ProductGroupActionTypes from 'actions/ProductGroupActions') and use them like ProductGroupActionTypes.LOAD_PRODUCTGROUP.
Usually, first one (without outcome suffix) is the one to initiate action and set some pending flag in reducer to show loader and also to initiate http calls in #Effects.
Second one, with success suffix is handled in reducer to change state.
Third one is error handling, whatever way you want to do it.
I am stuck here on how to mount the redux-form reducer at the right place.
I have this initial state for a namespace called person (among many other namespaces in the app), something like this:
{
personList: [],
creatingNewPerson: false,
newPerson: {}
}
the newPerson state is a form. How can I tell to have a redux-reducer acting on the newPerson state alone?
sure, you could do something like
combineReducers({
person: personReducer, // that's a reducer using the above json
newPerson: formReducer // import { reducer as formReducer } from 'redux-form'
})
but that's not the structure I am after. The state for the newPerson will be managed outside of the person state. But I want it to be managed inside.
It should be possible when the states redux-form is managing are JSON serializable.
How can this be achieved? Hope I made myself clear enough?
Unfortunately redux-form seems quite opinionated in this particular case, and the general sentiment from the documentation is quite clear: You should mount the formReducer at the root of your state tree under the form -key.
The reason for this is simple: the formReducer will handle the state for all of your forms, not just the new person form. So the state will look something like this:
{
person: { ... person-related state ... },
form: {
NewPersonForm: { ... new person form state ... },
SomeOtherForm: { ... some other form state ... },
...
NthAdditionalForm: { ... nth additional form state ... }
}
}
This means that if you'd want to position the state for each form nested inside the reducer the resulting object will end up in, you'd have to add in instances of formReducer in multiple locations, which would unnecessarily complicate your state.
My recommendation: Eat your proverbial greens in this case and just insert the formReducer in the default location, because that way you'll get the enjoy the power of redux-form without any additional headaches.
Now, after reading the above, if you're still dead set on actually mounting the formReducer somewhere deep within the damp and dark mazes of your state tree, you could do something like the following:
combineReducers({
persons: combineReducers({
person: personReducer,
newPerson: formReducer
}),
other: otherReducer,
...
some: someReducer
})
Then you also need to pass a getFormState to each reduxForm -wrapped component so they know where you hid their state:
const getFormState = state => {
// return the slice of state where we hid formReducer
return state.persons.newPerson
}
reduxForm({ getFormState })(SomeForm)
Doing this is something I cannot, with good conscience, recommend, but should produce the results you want (in addition to possible nasty side-effects if you ever add more than this one form to your app).
Hope this helps!
Is there any inconvenient at all if I design my reducers to, instead of reading only the partial state, had access to the full state tree?
So instead of writing this:
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
I destructure state inside doSomethingWithA, c or processB reducers, separately:
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state, action), // calc next state based on a
b: processB(state, action), // calc next state based on b
c: c(state, action) // calc next state based on a, b and c
}
}
Would I'd be using more RAM? Is there any performance inconvenient? I understand that in javascript, a reference is always passed as parameter, that's why we should return a new object if we want to update the state or use Immutable.JS to enforce immutability, so... again, would it be of any inconvenient at all?
No, there's nothing wrong with that. Part of the reason for writing update logic as individual functions instead of separate Flux "stores" is that it gives you explicit control over chains of dependencies. If the logic for updating state.b depends on having state.a updated first, you can do that.
You may want to read through the Structuring Reducers section in the Redux docs, particularly the Beyond combineReducers topic. It discusses other various reducer structures besides the typical combineReducers approach. I also give some examples of this kind of structure in my blog post Practical Redux, Part 7: Form Change Handling, Data Editing, and Feature Reducers.
Let's pretend I have a long-running function working on computing my new state.
Meanwhile another action comes in and changes the state while the first one did not finish and is working on stuff.
If I am imagining things correctly there is no actions queue and the state might be resolved in some unpredictable manner.
Should I be worried about this at all?
I don't mean real threads, just a concept for the lack of better wording. Actions are asynchronous and state keys are being accessed by reference.
I was concerned about the same thing so I just did some digging. It looks like two threads concurrently calling dispatch() (if it were possible) could raise an exception. But it shouldn't be possible and that error message points to a particular, different cause. The "actions queue" is in the browser's own event loop. That event loop runs async/interaction callbacks (from which we call dispatch()) one-at-a-time.
That's the responsibility of your own action creators and your own reducers, and heavily related to how you structure your actions and reducers conceptually. The Redux FAQ question on structuring "business logic" is very relevant here:Redux FAQ
Thunk action creators have access to getState, so it's very common to have a thunk check the current state and only dispatch under certain conditions, such as this example:
// An example of conditional dispatching based on state
const MAX_TODOS = 5;
function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState();
if(state.todos.length < MAX_TODOS) {
dispatch({type : "ADD_TODO", text : todoText});
}
}
}
Your reducer can also have sanity checks as well:
function todosReducer(state, action) {
switch(action.type) {
case "ADD_TODO": {
if(state.todos.length >= state.maxTodos) {
return state;
}
return {
...state,
todos : state.todos.concat(action.newTodo)
}
}
default : return state;
}
}
Personally, I don't like to have my reducers just blindly merge in whatever data's in the action, unless it's very small (like, say, the name of the currently selected tab or something). I prefer to have a reasonable amount of logic in my action creator to set up the action, a minimal-ish amount of data included in the action itself, and a sufficiently smart reducer to do the work based on that action.