Updating normalised data without causing more re-renders outside of the state slice that has been updated - redux

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.

Related

Redux Toolkit says this snippet(19 lines) is shorter code (vs. original 12) confusion

I am reading this page getting into react-redux https://redux.js.org/introduction/getting-started
I am very confused looking at the Basic Example which has 12 lines of code(excluding usage, imports, and comments)
Then I read this line on the "Redux Toolkit Example" which below the code states "Redux Toolkit allows us to write shorter logic that's easier to read, while still following the same Redux behavior and data flow." However, this example is 19 lines of code(excluding usage, imports, and comments)
The usage in both examples is 3 lines of code. Could someone explain this to me?
Perhaps when it scales, the redux toolkit example does save more code? Honestly, I find the Basic Example MUCH easier to read and maintain. NOTE: I am a complete newb which leads me to believe the basic example may be better as we ramp up and hire developers. This allows them to ramp up more quickly(but I am only a single data point amongst newbs)
thanks,
Dean
You're right about the lines of code in the example. Perhaps that simple counter example doesn't do justice to how much code Redux Toolkit can save because they aren't adding all the "bells and whistles" in their non-toolkit version.
This section is called "getting started with Redux" rather than "migrating to Redux Toolkit" so I suspect they don't want to overwhelm the user by introducing best practices like action creator functions which aren't strictly necessary. But you're not seeing the "write less code" benefit because most of the code that you can remove with the Toolkit is coming from things that weren't in the example in first place.
Action Creators
One of the main benefits of the createSlice function is that it automatically creates the action name constants and action creator functions to go along with each case in the reducer.
This example is just dispatching raw actions directly with string names store.dispatch({ type: 'counter/incremented' }). Most devs would not do this because of how fragile and inflexible it is.
An alternate version of the non-toolkit example, what you would see in most code, looks more like this:
// action name constants
const INCREMENTED = 'counter/incremented';
const DECREMENTED = 'counter/decremented';
// action creator functions
// usually most take some arguments
const incremented = () => ({
type: INCREMENTED,
})
const decremented = () => ({
type: DECREMENTED,
})
// reducer function
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case INCREMENTED:
return { value: state.value + 1 }
case DECREMENTED:
return { value: state.value - 1 }
default:
return state
}
}
If you want to include typescript types it gets even worse.
Immutability
The reducer itself could get really lengthy if you are trying to do immutable updates on deeply nested data.
Here's an example copied from those docs on how to safely update the property state.first.second[someId].fourth
Without Toolkit
function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
With Toolkit:
const reducer = createReducer(initialState, {
UPDATE_ITEM: (state, action) => {
state.first.second[action.someId].fourth = action.someValue
}
})
configureStore
The Toolkit configureStore actually does save a step vs the Redux createStore function when you are combining more than one reducer. But again this example fails to show it. Instead the Toolkit version is longer because we set a reducer property rather than just passing the reducer.
A typical Redux app uses the combineReducers utility to combine multiple reducers as properties on an object:
import {createStore, combineReducers} from "redux";
const rootReducer = combineReducers({
counter: counterReducer,
other: otherReducer
});
const vanillaStore = createStore(rootReducer);
With the Toolkit you can just pass your reducers map directly without calling combineReducers.
import {configureStore} from "#reduxjs/toolkit";
const toolkitStore = configureStore({
reducer: {
counter: counterReducer,
other: otherReducer
}
});
Which is roughly the same amount of code. But it also includes some default middleware which would be extra lines in the non-toolkit example.

React Redux accessing dynamically filtered state in mapStateToProps - rerendering woes

I have a functional component, that is passed instructions on what to pull from the redux store.
Using mapStateToProps=(state, ownProps), I can happily pull the required items from state (store) - but, at a cost of any changes in the entire state tree triggering rerunning mapStateToProps and a gazillion rerenders.
Let me unpack.
Here's a snapshot of part of the store:
{
settings: {...stuff...},
projects: [...stuff...],
definitions: [...stuff...],
themes: [...stuff...],
surfaces: {
'6': { <--- VARIABLE PASSED TO COMPONENT
surface: {
STRIP: [..stuff..],
GLOBAL: { <--- CATEGORY PASSED TO COMPONENT
DISPLAY: {...stuff...},
ASSIGNMENT: { <--- LIST OF REQUIRED OBJECTS HAS
A_TRACK: { SUBCATEGORY AND TARGET (A_TRACK etc...)
value: 0,
type: 'switch',
label: 'TRACK'
},
A_SEND: { <--- ANOTHER OBJECT I NEED TO GET
value: 0,
type: 'switch',
label: 'SEND'
},
A_PAN: {
value: 0,
type: 'switch',
label: 'PAN'
},
},
FADER_BANKS: {...stuff...},
STATUS: {...stuff...},
LOTS_MORE_STUFF
My parent component passes the required instructions to the child.
<RefMixerGroup
portId = {this.props.portId}
items={[
{parent: 'GLOBAL', group: "ASSIGNMENT", target: "A_TRACK"},
{parent: 'GLOBAL', group: "ASSIGNMENT", target: "A_SEND"},
]
}
/>
mapStateToProps is pretty simple:
const mapStateToPropy = (state, ownProps) => {
return {
groupItems: getItemsFromState(state.surfaces[ownProps.portId].surface, ownProps.items)
}
}
and the work is done in a simple function:
const getItemsFromState = (subState, items)=>{
let groupItems=[]
for (let i = 0; i < items.length; i++) {
const item = items[i];
const base = subState[item.parent];
let groupItem = base[item.group][item.target]
groupItems.push({...groupItem, target: item.target})
}
return groupItems
}
But because I am creating this array of matches, I think redux thinks I should be subscribing to every item in the tree...when I only want changes on the found elements, in this case:
surfaces[6].surface[GLOBAL][ASSIGNMENT][A_TRACK]
surfaces[6].surface[GLOBAL][ASSIGNMENT][A_SEND]
I tried using reselect and the rereselect instead of my getItemsFromState function above,
but all with the same result. Any change in that tree, starting with surfaces[6] triggers mapsStateToProps and a rerender.
There must be way around this, but I can't figure it out. I tried using areStatesEqual but it only provides nextState and prevState, and I need ownProps to compute equality. I possibly could use areStatePropsEqual, but that only works AFTER recomputing mapStateToProps unnecessarily.
There must be a way!
getItemsFromState is creating a new groupItems array reference every time it runs. It will be called after every dispatched action. Since connect re-renders any time any of the fields returned by mapState have changed to a new reference, your code is forcing React-Redux to re-render every time.
This is specifically why you should use memoized selectors to only return new derived data references if the input references have changed, typically with Reselect's createSelector. If your use of Reselect isn't helping here, it's likely that your selectors aren't being set up correctly, but I'd need to see specific examples to give advice there.
It's also why components should subscribe to the smallest amount of data that they actually need.
If you are using a function component, I'd suggest using useSelector instead of connect as well.

Access other slice of state in reducer using #ngrx/store

Given the following (and assuming we cannot change the state's structure):
StoreModule.forRoot({
a: aReducer,
b: {
b1: b1Reducer,
b2: b2Reducer
}
});
and b1Reducer is dependent on the value of a (for example because it contains something like user info).
What is the most idiomatic way to access (read-only) a in b1Reducer?
The solution I came up with is using #ngrx/effects, dispatch another action with a that can be used in the reducer:
#Effect()
augmentAction$ = this.action$
.ofType(Actions.Action1)
.withLatestFrom(this.store$)
.switchMap(([action, state]:[Action, AppState]) => {
const a = state.a;
return [new Actions.Action2(a)];
});
This works, but it becomes hard to manage if almost every action needs to be redispatched if a is used in many reducers. Is there a better way to handle this?

How to mount the redux-form further down in a nested state structure?

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!

Redux with Immutable JS

When using immutablejs with Redux, we will get a regular javascript object back from combineReducers, meaning it won't be an immutable data structure even if everything within it is. Doesn't this mean that using immutablejs will be in vain since a whole new state object will be created on every action anyhow?
Example:
const firstReducer = (state = Immutable.Map({greeting : 'Hey!'})) => {
return state
}
const secondReducer = (state = Immutable.Map({foo : 'bar'})) => {
return state
}
const rootReducer = combineReducers({
firstReducer, secondReducer
})
a whole new state object will be created on every action
Yes but the slices of that state that are assigned by combineReducers are not recreated. It's sort of analogous to doing this:
const person = { name: 'Rob' };
const prevState = { person };
const nextState = { person };
I created a new state object (nextState), but its person key is still set to the same exact object as was prevState's person key. There is only one instance of the string 'Rob' in memory.
The problem is when I mutate the person object, I'm changing it for multiple states:
const person = { name: 'Rob' };
const prevState = { person };
person.name = 'Dan'; // mutation
const nextState = { person };
console.log(prevState.person.name); // 'Dan'
Coming back to Redux, once all reducers have been called for the first time, they will have initialized their slices of the application's state, and your application's entire overall state would basically be equal to this:
{
firstReducer: Immutable.Map({greeting : 'Hey!'}),
secondReducer: Immutable.Map({foo : 'bar'}),
}
Note that this is a normal object. It has properties that hold Immutable objects.
When an action is dispatched and goes through each reducer, the way you've got it, the reducer simply returns the existing Immutable object back again, it doesn't create a new one. Then the new state is set to an object with a property firstReducer simply pointing back to the same Immutable object that the previous state was pointing to.
Now, what if we didn't use Immutable for firstReducer:
const firstReducer = (state = {greeting : 'Hey!'}) => {
return state
}
Same idea, that object that is used as the default for state when the reducer is first called is just passed from the previous state to the next state. There is only ever one object in memory that has the key greeting and value Hey!. There are many state objects, but they simply have a key firstReducer that points to the same object.
This is why we need to make sure we don't accidentally mutate it, but rather replace it when we change anything about it. You can accomplish this without Immutable of course by just being careful, but using Immutable makes it more foolproof. Without Immutable, it's possible to screw up and do this:
const firstReducer = (state = {greeting : 'Hey!'}, action) => {
switch (action.type) {
case 'CAPITALIZE_GREETING': {
const capitalized = state.greeting.toUpperCase();
state.greeting = capitalized; // BAD!!!
return state;
}
default: {
return state;
}
}
}
The proper way would be to create a new state slice:
const firstReducer = (state = {greeting : 'Hey!'}, action) => {
switch (action.type) {
case 'CAPITALIZE_GREETING': {
const capitalized = state.greeting.toUpperCase();
const nextState = Object.assign({}, state, {
greeting: capitalized,
};
return nextState;
}
default: {
return state;
}
}
}
The other benefit Immutable gives us is that if our reducer's slice of the state happened to have a lot of other data besides just greeting, Immutable can potentially do some optimizations under the hood so that it doesn't have to recreate every piece of data if all we did was change a single value, and yet still ensure immutability at the same time. That's useful because it can help cut down on the amount of stuff being put in memory every time an action is dispatched.
combineReducers() that comes with Redux indeed gives you a plain root object.
This isn’t a problem per se—you can mix plain objects with Immutable objects as long as you don’t mutate them. So you can live with this just fine.
You may also use an alternative combineReducers() that returns an Immutable Map instead. This is completely up to you and doesn’t make any big difference except that it lets you use Immutable everywhere.
Don’t forget that combineReducers() is actually easy to implement on your own.
Doesn't this mean that using immutablejs will be in vain since a whole new state object will be created on every action anyhow?
I’m not sure what you mean by “in vain”. Immutable doesn’t magically prevent objects from being created. When you set() in an Immutable Map, you still create a new object, just (in some cases) more efficiently. This doesn’t make a difference for the case when you have two keys in it anyway.
So not using Immutable here doesn’t really have any downsides besides your app being slightly less consistent in its choice of data structures.

Resources