Search code examples
javascriptreactjsjavascript-objectssetstate

React setState of for deeply nested value


I’ve got a very deeply nested object in my React state. The aim is to change a value from a child node. The path to what node should be updated is already solved, and I use helper variables to access this path within my setState.

Anyway, I really struggle to do setState within this nested beast. I abstracted this problem in a codepen: https://codesandbox.io/s/dazzling-villani-ddci9

In this example I want to change the child’s changed property of the child having the id def1234.

As mentioned the path is given: Fixed Path values: Members, skills and variable Path values: Unique Key 1 (coming from const currentGroupKey and both Array position in the data coming from const path

This is my state object:

constructor(props) {
  super(props);
  this.state = { 
    group:
    {
      "Unique Key 1": {
        "Members": [
          {
            "name": "Jack",
            "id": "1234",
            "skills": [
              {
                "name": "programming",
                "id": "13371234",
                "changed": "2019-08-28T19:25:46+02:00"
              },
              {
                "name": "writing",
                "id": "abc1234",
                "changed": "2019-08-28T19:25:46+02:00"
              }
            ]
          },
          {
            "name": "Black",
            "id": "5678",
            "skills": [
              {
                "name": "programming",
                "id": "14771234",
                "changed": "2019-08-28T19:25:46+02:00"
              },
              {
                "name": "writing",
                "id": "def1234",
                "changed": "2019-08-28T19:25:46+02:00"
              }
            ]
          }
        ]
      }
    }
  };
}

handleClick = () => {
  const currentGroupKey = 'Unique Key 1';
  const path = [1, 1];
  // full path: [currentGroupKey, 'Members', path[0], 'skills', path[1]]
  // result in: { name: "writing", id: "def1234", changed: "2019-08-28T19:25:46+02:00" }

  // so far my approach (not working) also its objects only should be [] for arrays
  this.setState(prevState => ({
    group: {
      ...prevState.group,
      [currentGroupKey]: {
        ...prevState.group[currentGroupKey],
        Members: {
          ...prevState.group[currentGroupKey].Members,
          [path[0]]: {
            ...prevState.group[currentGroupKey].Members[path[0]],
            skills: {
              ...prevState.group[currentGroupKey].Members[path[0]].skills,
              [path[1]]: {
                ...prevState.group[currentGroupKey].Members[path[0]].skills[
                  path[1]
                ],
                changed: 'just now',
              },
            },
          },
        },
      },
    },
  }));
};

render() {
  return (
    <div>
      <p>{this.state.group}</p>
      <button onClick={this.handleClick}>Change Time</button>
    </div>
  );
}

I would appreciate any help. I’m in struggle for 2 days already :/


Solution

  • Before using new dependencies and having to learn them you could write a helper function to deal with updating deeply nested values.

    I use the following helper:

    //helper to safely get properties
    // get({hi},['hi','doesNotExist'],defaultValue)
    const get = (object, path, defaultValue) => {
      const recur = (object, path) => {
        if (object === undefined) {
          return defaultValue;
        }
        if (path.length === 0) {
          return object;
        }
        return recur(object[path[0]], path.slice(1));
      };
      return recur(object, path);
    };
    //returns new state passing get(state,statePath) to modifier
    const reduceStatePath = (
      state,
      statePath,
      modifier
    ) => {
      const recur = (result, path) => {
        const key = path[0];
        if (path.length === 0) {
          return modifier(get(state, statePath));
        }
        return Array.isArray(result)
          ? result.map((item, index) =>
              index === Number(key)
                ? recur(item, path.slice(1))
                : item
            )
          : {
              ...result,
              [key]: recur(result[key], path.slice(1)),
            };
      };
      const newState = recur(state, statePath);
      return get(state, statePath) === get(newState, statePath)
        ? state
        : newState;
    };
    
    //use example
    const state = {
      one: [
        { two: 22 },
        {
          three: {
            four: 22,
          },
        },
      ],
    };
    const newState = reduceStatePath(
      state,
      //pass state.one[1],three.four to modifier function
      ['one', 1, 'three', 'four'],
      //gets state.one[1].three.four and sets it in the
      //new state with the return value
      i => i + 1 // add one to state.one[0].three.four
    );
    console.log('new state', newState.one[1].three.four);
    console.log('old state', state.one[1].three.four);
    console.log(
      'other keys are same:',
      state.one[0] === newState.one[0]
    );