redux: set state in leaf node of large state tree - redux

I have an Immutable object that holds other Immutable objects, and the redux state tree is set to be an instance of the topmost object:
records.js:
const TypeC = newRecord({
content: null
})
const TypeB = newRecord({
typeC: new TypeC;
})
export const TypeA = newRecord({
typeB: new TypeB;
})
reducer.js:
import {
TypeA,
} from './records';
import types from './types';
const applicationReducer = (state = new TypeA(), action) => {
switch (action.type) {
...
}
default:
return state;
}
};
My question is: how do I write reducer code to change the content field in TypeC? I've tried something like
CASE types.UPDATECONTENT: {
return state.get('typeB').get('typeC').set('content', "new content")
}
but this seems to only return the TypeC segment of my state tree, when I need the entire tree from root.

Pattern for nested objects explained here:
https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns/

state.get('typeB').get('typeC').set('content', "new content") goes down the tree and modifies the attribute "content" of typeC. But since immutable is immutable, it does not modify anything, it returns a new record which has the updated value (!). The things between state and 'typeC' are not touched. Furthermore, set returns the newly created record, so you effectively overwrite state with the updated typeC.
An easy way to update nested objects is using the "deep persistent changes" that are named <operation>In, e.g. setIn. It takes care of creating updated intermediate objects too.
TL/DR:
CASE types.UPDATECONTENT: {
return state.setIn(['typeB', 'typeC','content'], "new content");
}

Related

When do you use updater() and patchstate() in ngrx/component-store?

I'm using ngrx/component-store and loving it so far. Having prior store knowledge building my own simple ones, the only real headache I've had so far is when I've had to update an array and figured out I have to always create a new one for the internal compare() pipe to realize the array got updated.
Anyway, reading through the documentation it talks about updater methods and patchState. To me they do exactly the same thing, but their creation is slightly different. You would call patchState inside of a method while this.updater() returns a method giving you a function you can expose in your service. Anytime I'm updating my state it's always after a network call. I assume there are plenty of scenarios where you'd want to update your state without a network call so this is why you would want to have an updater available to your component to call. The question is if an updater and patchState are really doing the same thing then is it a better practice to call an updater in an effect or use patchState, or maybe am I putting too much logic in my effect?
On a side note, the docs say an updater method is supposed to be a pure function. If you're using it to your push an object onto an array then is it really pure?
// adding the selectors so people know what components are subscribing to
readonly approvals$ = this.select(state => state.requestApprovals);
readonly registration$ = this.select(state => state);
readonly updateAssessment = this.effect(($judgement: Observable<{id: string, isApproved: boolean}>) => {
return $judgement.pipe(
switchMap((evaluation) => {
const state = this.get();
return this.requestApproval.patch(state.id, state.companyName, evaluation.id, evaluation.isApproved).pipe(
tapResponse(
(result) => {
// is it better to call patchState()?
this.patchState((state) => {
for(let i = 0; i < state.requestApprovals.length; i++) {
if(state.requestApprovals[i].id == result.id) {
state.requestApprovals[i].isApproved = result.isApproved;
}
}
// the take away is you must assign a whole new array object when you update it.
state.requestApprovals = Object.assign([], state.requestApprovals);
return state;
});
// or this updater?
// this.applyDecisionPatch(evaluation);
},
// oh look! another updater reassigning my array to the state so
// it propagates to subscribers to reset the UI
() => { this.reverseDecision(); }
)
);
})
);
});
// this is private to make sure this can only be called after a network request
private readonly applyDecisionPatch = this.updater((state, value: {id: string, isApproved: boolean}) => {
for(let i = 0; i < state.requestApprovals.length; i++) {
if(state.requestApprovals[i].id == value.id) {
state.requestApprovals[i].isApproved = value.isApproved;
}
}
state.requestApprovals = Object.assign([], state.requestApprovals);
return state;
});
Note: There's no tag for ngrx-component-store so couldn't tag it.
An updater can be compared to a reducer.
All the options to modify the state should change it in an immutable way.
A library like ngrx-immer can be used to make this easier.
The main difference is that updater receives the current state, and you can change the state based on it. E.g. a conditional update, or can be used with #ngrx/entity
While with setState and patchState, you just set state properties.
setState updates the whole state object, whereas patchState only sets the given properties and doesn't touch the rest of the state object.
These two methods are also easier to use when you just want to set the state, because you don't have to create an updater function.
To answer the side question, push is not immutable. Instead of creating a new instance, it updates the array instance.

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

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.

Flow doesn't infer type correctly

I define an multiple subtypes in the Action creator in redux:
Action creator:
export type Action = { type: "SELECT", index: number } | { type: "OTHER" };
Reducer:
module.exports = (state: string = "", action: Action): string => {
switch (action.type) {
case "SELECT":
return action.index;
default:
return state;
}
};
but if I define SELECT in a constant const select = "SELECT" and implement it in the code above I obtain an error message:
property `index`. Property not found in object type
Note: Taking flow-pattern as it is F8 app:
https://github.com/fbsamples/f8app/blob/master/js/actions/types.js
How should implement it by avoiding having "SELECT" keyword both in the action and in the reducer?
You would normally have another constant with the action type, which would be used for both your action and reducer.
const SELECT = 'SELECT';
or even better (to avoid any conflicts):
const SELECT = 'redux/<module>/SELECT';
In the same action file or in another (that's totally up to you). Then just export like export const SELECT, and import like import { SELECT } from './constants'.
You should also take a look at redux-ducks, might be of your interest.
EDIT:
They use bitwise OR to group all possible different action cases. That helps testing whether the code is syntactically correct with flow (by setting their dispatched actions to match type Action). See their full explanation here.
That does not take away the fact that they have to dispatch the action with their desired action type though. login

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.

How to share state between 2 combine reducers?

I have 2 reducers that I use and combine them. In the first reducer, I have something that gets all the initial data (which is also relevant for the second reducer).
How do I use the data in the state that I initialize/set from the first reducer to the second one?
function reducer1(state = initialState, action = '') {
switch (action.type) {
case constants.INITIAL_DATA:
returnstate.set('data', fromJS(document.data));
....
Then I combine both of those reducers and I want to access "data" from both of them (or pass the data as initialState to the second reducer).
A reducer should return a plain object and have no side effects. If you want the same state available in 2 different action.type cases, then they either need to be in the same function, or a parent function needs to pass the state to different reducer functions, like so:
function parentReducer(state = {}, action = '') {
switch (action.type) {
case CASE_ONE:
return childReducer1(state, action)
case CASE_TWO:
return childReducer2(state, action)
default:
return state
}
}
But this brings up an important design point: each (top-level) reducer represents a distinct branch of the Redux state tree, and you can probably always design your data store in a way that different parts of the tree don't need to be shared. In Redux (check out the DevTools), you have a single object, and the top-level keys of this object are the names of your top-level reducer functions.
Basically, if you perceive a need to set a different state in a reducer, so other reducers can use that, it evidence of a need to rethink the store's design.

Resources