Sometimes reducers get kind of messy:
const initialState = {
notificationBar: {
open: false,
},
};
export default function (state = initialS
You can use Lenses.
import { set, makeLenses } from '@DrBoolean/lenses'
const L = makeLenses(['notificationBar', 'open']);
const notificationBarOpen = compose(L.notificationBar, L.open)
const setNotificationBarOpenTrue = set(notificationBarOpen, true)
const a = { notificationBar: { open: false } }
const b = setNotificationBarOpenTrue(a)
// `a` is left unchanged and `b` is `{ notificationBar: { open: true } }`
You can think about Lenses as compositional property access/update.
Some good resources about Lenses:
If you are ok with reading lisps, I would also recommend taking look at this excellent introduction to lenses from racket docs. Finally, if you want to go deeper and are ok with reading haskell you can watch: Lenses - compositional data access and manipulation.
UPD: it's now a part of the ES2018
It might be slightly improved via a non-standardised yet properties spread syntax:
return {
...state,
notificationBar: {
...state.notificationBar,
open: true,
},
};
In addition to what've been said earlier, here is a functional way with Ramda:
import { assocPath } from 'ramda';
const o1 = { a: { b: { c: 1 }, bb: { cc: 22 } } };
const o2 = assocPath(['a', 'b', 'c'])(42)(o1);
console.log(o1 !== o2, o1.a !== o2.a); // new copies of "changed" objects
console.log(o1.a.bb === o2.a.bb); // deep unchanged properties are copied by reference
Although it's possible to use the spread operator, there are lots of other ways to achieve the same result without even needing a future JS compiler for a non standardised feature. Here are some other options in no particular order.
If you are sure that your state won't grow, then you can simply return the entire new state as a literal.
return {
notificationBar: {
open: true
}
}
However, that's not often going to be appropriate because it's unlikely that your state will be this simple.
Redux gives you a utility method for combining several reducers that work on different parts of the state object. In this case, you'd create a notificationBar
reducer that handled this object alone.
createStore(combineReducers({
notificationBar: function(state=initialNBarState, action) {
switch (action.type) {
case actions.LAYOUT_NOTIFICATIONBAR_OPEN:
return Object.assign({}, state, { open: true });
}
});
This prevents you from having to worry about the top level of properties, so that you can avoid nesting calls to Object.assign
.
If your state can logically broken down into clearly defined sections then this is probably the most idiomatic way to solve this problem.
You can use an persistent data structures library to create data structures than can be modified to return a copy.
Mori is the result of compiling Clojure's data structures and functional API into JS.
import { hashMap, updateIn } from 'mori';
const initialState = hashMap(
"notificationBar", hashMap(
"open", false
)
);
// ...
return updateIn(state, ['notificationBar', 'open'], true);
ImmutableJS is a more imperative approach to bringing the semantics of Hash Array Mapped Tries from Clojure's persistent data structures to Javascript.
import { Map } from 'immutable';
const initialState = Map({
notificationBar: Map({
open: true
});
});
// ...
return state.setIn(['notificationBar', 'open'], true);
Object.assign
You can create a friendlier version of Object.assign
to write terser versions of the code above. In fact, it can be nearly as terse as the ...
operator.
function $set(...objects) {
return Object.assign({}, ...objects);
}
return $set(state, {
notificationBar: $set(state.notificationBar, {
open: true,
})
});
There are a number of libraries that also offer immutability helpers for making modifications to regular mutable objects.
React has had a built in set of immutability helpers for a long time. They use a similar syntax to MongoDB queries.
import update from 'react-addons-update';
return update(state, {
notificationBar: {
open: { $set: true }
}
});
This library allows you to use familiar dot paths to specify updates to (nested) properties.
import dotProp from 'dot-prop-immutable';
return dotProp.set(state, 'notificationBar.open', true);
This library is a wrapper around react-addons-update
and provides a more functional syntax for updating (nested) properties.
Rather than passing a new value, you pass a function which takes the old value and returns a new one.
import updateIn from 'update-in';
return updateIn(state, ['notificationBar', 'open'], () => true);
For updating properties, this library is like a cross between dot-prop-immutable
and update-in
.
import path from 'immutable-path';
return path.map(state, 'notificationBar.open', () => true);
If you're using Immutable.js, you can look under Nested Structures topic some functions that may help you, I personally use mergeDeep
:
prevState.mergeDeep({ userInfo: {
username: action.payload.username,
} }),
All advice here are great and valid, but I would like to offer another solution. The problem which appears here is definitely a common pattern, so I think it is much better just to write your own interface for such updates and stick with it inside reducers, and use one function to update deeply inside all your reducers.
For example, I have created a library, where I tried to address this issue the next way: I get the type of the module (so-called "tile"), function to perform operations (both async and sync) and desired nesting based on passed params. So, for your case it will be something like:
import { createSyncTile } from 'redux-tiles';
const uiTile = createSyncTile({
type: ['ui', 'elements'],
fn: ({ params }) => params,
// type will be `notificationBar`
nesting: ({ type }) => [type],
});
And that's it -- it will be update correctly on arbitrary nesting. Also, tile provides selectors, so you don't have to worry personally where precisely data is located, you can just use them. So, I don't want to say it is the best solution, but the idea is pretty simple -- don't be afraid to write your own implementation, and then just use factory to solve this issue.