2

I am trying to update a value stored in a deep nested object. It contains many pieces of information and the schema is fixed. I am trying to copy the object then return the object with update value onChange from an input. However I am unable to successfully correctly copy the full tree and return the updated content.

DEMO: https://codesandbox.io/s/4j7x8jlk9w

the object looks like:

content: {
    label: "Label",
    templates: [
      {
        name: "example",
        type: "the type",
        items: [
          {
            key: "key1",
            properties: {
              text: {
                label: "The Label 1",
                value: "The Value 1"
              },
              color: {
                label: "Color",
                value: "#123"
              }
            }
          },
          {
            key: "key2",
            properties: {
              text: {
                label: "The Label 2",
                value: "The Value 2"
              },
              color: {
                label: "Color",
                value: "#456"
              }
            }
          }
        ]
      }
    ]
  }

The Reducer:

case "UPDATE_VALUE":
      const content = state.content.templates[state.templateKey].items[
        state.itemKey
      ].properties.text.value =
        action.value;

      return { ...state, content };

    default:
      return state;
  }

The Component:

import React, { PureComponent } from "react";
import { connect } from "react-redux";

import { updateValue } from "./actions";

class Page extends PureComponent {
  render() {
    const { content, templateKey, itemKey } = this.props;

    return (
      <div>
        <h1
          style={{
            color:
              content.templates[templateKey].items[itemKey].properties.color
                .value
          }}
        >
          {content.templates[templateKey].items[itemKey].properties.text.value}
        </h1>
        <input
          name={content.templates[templateKey].items[itemKey].key}
          value={
            content.templates[templateKey].items[itemKey].properties.text.value
          }
          onChange={e => this.props.updateValue(e.target.name, e.target.value)}
        />
      </div>
    );
  }
}

const mapStateToProps = state => ({
  content: state.content,
  templateKey: state.templateKey,
  itemKey: state.itemKey
});

const mapDispatchToProps = dispatch => ({
  updateValue: (key, value) => dispatch(updateValue(key, value))
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Page);
1
  • You need to create a completely new content tree with the updated values. You shouldn't be mutating that item. Commented Sep 22, 2018 at 17:44

3 Answers 3

4

With deeply nested data like this, you can use the spread syntax at each depth of the tree until you arrive at the thing you want to change. For arrays, you can use slice to create a copy of the array without mutating it.

https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#correct-approach-copying-all-levels-of-nested-data

and https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#inserting-and-removing-items-in-arrays

will be resources to help you understand this better.

Let's assume your reducer is given an index for which template to update, and an index for which item to update within that template. Your code might look like this:

return {
    ...state,
    templates: [
      ...state.templates.slice(0, templateIndex),
      {
        ...state.templates[templateIndex],
        items: [
          ...state.templates[templateIndex].items.slice(0, itemIndex),
          {
            ...state.templates[templateIndex].items[itemIndex],
            value: action.value 
          },
          ...state.templates[templateIndex].items.slice(itemIndex)
        ]
      },
      ...state.templates.slice(templateIndex)
    ]
  }

As you can see, it gets pretty messy when you're dealing with nested data like this. It's recommended that you normalize nested data to make your reducers have to do less work to find the thing to change.

Here's your updated codesandbox: https://codesandbox.io/s/w77yz2nzl5

Sign up to request clarification or add additional context in comments.

6 Comments

I find it easier to use map/filter to perform array updates. Syntax is a lot easier on the eyes.
In the case where you're updating multiple instances I think that makes sense. But if you're only updating a single array element, you'd have to have a condition in map/filter to only update the targeted index, so at that point seems like you might as well just slice. I agree though this looks super ugly.
this hurts my brain to look at, but works the best and aligns to Redux guidelines. Mohammad's suggestion works but is a shallow copy of the object which is not recommended.
It's definitely an eyesore. Consider modeling your store differently to make it easy for you to make updates in the future. IE, think about how you can flatten the data structure as much as possible, while retaining the same meaning.
@ZackTanner okay great. i will look into this. do you know of a good example? i have to transfer the templates obj back to the db, so i am fumbling with the idea of breaking up past that.
|
-1

Have you tried using the spread operating to populate a new object with the contents of the current state of the old nested object, then you add the new updated values after? That way the fields you don't change remain the same. This isn't exact, but what I would do in your case is create a method on page.js like:

createNewValue = (e) => {
  const { content, templateKey, itemKey } = this.props;
  let newValues = {...content }
  newValues.templates[templateKey].items[itemKey].properties.text.value = e.target.value
  this.props.updateValue(newValues)
}

Then in your updateValue action creator you just pass in the new content object that retains the old values that you aren't updating, and includes the new value for the change you are making. You would fire off this method on page.js in your onChange handler.

2 Comments

This is still mutating items[itemKey].properties.text.value from the content ref, because the spread syntax just creates a shallow copy.
@ZackTanner whats the best way to create an immutable copy of a large object like this?
-1

first of all , you are not returning the proper content object , as you are returning the value of the update , so you should update that :

 const content = state.content.templates[state.templateKey].items[
    state.itemKey
  ].properties.text.value =
    action.value;

with :

  const content = state.content;
  content.templates[state.templateKey].items[
    state.itemKey
  ].properties.text.value =
    action.value;

The key would be either returning a new reference for content , as you are applying a selector in map state to props on the content itself ( or , as a much better solution , composing such reducer to contain multiple reducers) , ie :

 const content = {...state.content};
  content.templates[state.templateKey].items[
    state.itemKey
  ].properties.text.value =
  action.value;

or applying your selector on the values you want (ie more granular selectors)

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.