1

I have a nested state like this:

this.state = {
  fields: {
    subject: '',
    from: {
      name: '',
    },
  },
};

In an onChange function I'm handling updates to these nested values.

I'm trying to build a dynamically spread setState() for deep nests using dot notation.

For instance, with the array: const tree = ['fields','subject'] I'm able to update the subject state value with:

this.setState(prevState => ({
  [tree[0]]: {
    ...prevState[tree[0]],
    ...(tree[2] ? {
      ...prevState[tree[1]],
      [tree[2]]: value
    } 
    : { [tree[1]]: value })
  },
}));

Since the ternary operator is ending on { [tree[1]]: value }

But when my tree array is: const tree = ['fields','from','name'] the state value for fields.from.name is not changing, where it should be resolving to the first part of the ternary operator:

{
  ...prevState[tree[1]],
  [tree[2]]: value
}

Am I missing something?

alyx
  • 2,593
  • 6
  • 39
  • 64

2 Answers2

3

I've grown to prefer using libraries for these sorts of functions when it otherwise feels like I'm reinventing the wheel. lodash provides a set function (which also supports string paths):

_.set(object, path, value)

var object = { 'a': [{ 'b': { 'c': 3 } }] };

_.set(object, 'a[0].b.c', 4);
console.log(object.a[0].b.c);
// => 4

_.set(object, ['x', '0', 'y', 'z'], 5);
console.log(object.x[0].y.z);
// => 5

You'd also want to use _.cloneDeep(value) because _.set mutates the object.

this.state = {
  fields: {
    subject: '',
    from: { name: '' },
  },
};


const tree = ['fields', 'from', 'name']; // or 'fields.from.name'
this.setState(prevState => {
  const prevState_ = _.cloneDeep(prevState);
  return _.set(prevState_, tree, value);
});
Wex
  • 15,539
  • 10
  • 64
  • 107
  • Using pre-made, pre-tested functionality is good, though the deep clone is more costly than only cloning what changes... – T.J. Crowder Mar 08 '19 at 16:06
  • this isn't updating the state – alyx Mar 08 '19 at 16:08
  • 3
    To that end, something like Ramda's [`lensPath`](https://ramdajs.com/docs/#lensPath) together with [`set`](https://ramdajs.com/docs/#set) makes for a useful combination that only mutates what is necessary in such a set. But it is only good for when you want to replace the whole state object with a minimally-mutated version. Ramda won't mutate your data for you. ([`assocPath`](https://ramdajs.com/docs/#assocPath) is also helpful.) – Scott Sauyet Mar 08 '19 at 16:10
  • @alyx - I fixed a typo and added `return` to my function in `setState`. – Wex Mar 08 '19 at 16:11
  • 1
    @Wex - FWIW, I find it really handy to do a Stack Snippet so I catch it when I make those kinds of mistakes. (You probably know how, but if not, [here's how to do one](https://meta.stackoverflow.com/questions/358992/).) – T.J. Crowder Mar 08 '19 at 16:30
  • Thanks T.J. I'll use that next time. – Wex Mar 08 '19 at 16:31
2

You'll need a loop. For instance:

function update(prevState, tree, value) {
    const newState = {};
    let obj = newState;
    for (let i = 0; i < tree.length; ++i) {
        const name = tree[i];
        if (i === tree.length - 1) {
          obj[name] = value;
        } else {
          obj = obj[name] = {...prevState[name]};
        }
    }
    return newState;
}

Live Example:

this.state = {
  fields: {
    subject: '',
    from: {
      name: '',
    },
  },
};

function update(prevState, tree, value) {
    const newState = {};
    let obj = newState;
    let prev = prevState;
    for (let i = 0; i < tree.length; ++i) {
        const name = tree[i];
        if (i === tree.length - 1) {
          obj[name] = value;
        } else {
          const nextPrev = prev[name];
          obj = obj[name] = {...nextPrev};
          prev = nextPrev;
        }
    }
    return newState;
}

const tree = ['fields','from','name']
const value = "updated";
console.log(update(this.state, tree, value));

I'm sure that can be shoehorned into a call to Array#reduce (because any array operation can be), but it wouldn't buy you anything.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875