33

Sometimes reducers get kind of messy:

const initialState = {    notificationBar: {        open: false,    },};export default function (state = initialState, action) {  switch (action.type) {    case actions.LAYOUT_NOTIFICATIONBAR_OPEN:      return Object.assign({}, state, {        // TODO: Find a cleaner way to do this!        notificationBar: Object.assign({}, state.notificationBar, {          open: true,        }),      });    default:      return state;  }}

Is there a more terse way to do this?

Dan Prince's user avatar
Dan Prince
30k15 gold badges93 silver badges123 bronze badges
askedFeb 24, 2016 at 2:20
ffxsam's user avatar
4
  • 2
    For that very reason people use immutablejs (or any other similar library).CommentedFeb 24, 2016 at 2:22
  • 3
    Btw:return { ...state, notificationBar: { ...state.notificationBar, open: true, }, };CommentedFeb 24, 2016 at 2:22
  • D'oh! Of course.. spread operator.CommentedFeb 24, 2016 at 2:24
  • @zerkms The spread is a totally acceptable answer, BTW. Feel free to post it and I'll accept it.CommentedFeb 24, 2016 at 2:34

6 Answers6

58

UPD: it's now a part of the ES2018

It might be slightly improved via anon-standardised yet properties spread syntax:

return {    ...state,    notificationBar: {        ...state.notificationBar,        open: true,    },};
answeredFeb 24, 2016 at 2:35
zerkms's user avatar
Sign up to request clarification or add additional context in comments.

3 Comments

@jcubic thanks! And sorry for being picky, but it's not an "operator". I misused the term, it's just a "spread syntax" (so I also corrected my answer in that regards)
In the beginning of ECMAScript specification 9.0ecma-international.org/ecma-262/9.0/index.html they say "rest parameter and spread operator", don't know why MDN docs say "syntax".
@jcubic I think it's a mistake: from the language perspective it cannot be an operator, it's punctuation. I reported an issue about that, thanks!github.com/tc39/ecma262/issues/1295
47

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.

Return a Literal

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.


Combine Reducers

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 anotificationBar 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 toObject.assign.

If your state can logically broken down into clearly defined sections then this is probably the most idiomatic way to solve this problem.


Use Immutable Data

You can use an persistent data structures library to create data structures than can be modified to return a copy.

Mori

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

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

AliasObject.assign

You can create a friendlier version ofObject.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,  })});

Use Immutable Helpers

There are a number of libraries that also offer immutability helpers for making modifications to regular mutable objects.

react-addons-update

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 }  }});

dot-prop-immutable

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

update-in

This library is a wrapper aroundreact-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);

immutable-path

For updating properties, this library is like a cross betweendot-prop-immutable andupdate-in.

import path from 'immutable-path';return path.map(state, 'notificationBar.open', () => true);
zeckdude's user avatar
zeckdude
16.3k45 gold badges150 silver badges195 bronze badges
answeredFeb 24, 2016 at 2:56
Dan Prince's user avatar

10 Comments

And also various other libs similar to the React Helpers, such as dot-prop-immutable, update-in, immutable-path, and object-path-immutable.
you could useBoolean.bind(0,1) instead of an arrow/anon for those last couple...
Sure, but that's a pretty confusing way of expressing a function that always returnstrue. Not only is it easier to read and understand, but bound functions are also less performant and harder to debug.
@ffxsam It is for arrays, not for objects.
There's an error in your dot-prop-immutable example.. should bereturn dotProp.set(state, 'notificationBar.open', true); - missing .set. Sorry, SO won't let me edit the answer if less than 6 chars.
|
6

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 readinglisps, I would also recommend taking look at this excellentintroduction to lenses fromracket docs. Finally, if you want to go deeper and are ok with readinghaskell you can watch:Lenses - compositional data access and manipulation.

soundyogi's user avatar
soundyogi
3893 silver badges15 bronze badges
answeredDec 27, 2016 at 9:50
Safareli's user avatar

Comments

1

If you're usingImmutable.js, you can look underNested Structures topic some functions that may help you, I personally usemergeDeep:

prevState.mergeDeep({ userInfo: {  username: action.payload.username,} }),
answeredAug 31, 2017 at 19:20
Ricardo Mutti's user avatar

1 Comment

a.k.a. lodash's_.merge
0

In addition to what've been said earlier, here is a functional way withRamda:

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" objectsconsole.log(o1.a.bb === o2.a.bb); // deep unchanged properties are copied by reference
answeredSep 25, 2016 at 10:18
aeldar's user avatar

Comments

0

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.

answeredJun 4, 2017 at 14:48
Bloomca's user avatar

Comments

Your Answer

Sign up orlog in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

By clicking “Post Your Answer”, you agree to ourterms of service and acknowledge you have read ourprivacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.