简体   繁体   中英

React: issues updating array with useState hook

I have an array of objects representing the opening hours of a business, now the user is able to change the open and closing time of specific days and shifts, adding new shifts and removing them too, I'm still a newb at react and state hook with arrays and my changes are not reflected in my views.

The actions I'm trying to do are:

Updating a specific item property in array

openingTimes[dayIndex].hours[splitIndex].opens = selectedHour+':'+selectedMinute;

Removing last item of array

openingTimes[i].hours.pop();

Adding an item to array

openingTimes[i].hours.push({ opens: 'Abre', closes: 'Cierra' });

And the openingTimes array of objects for reference:

const [openingTimes, setOpeningTimes] = useState([
        {day:'monday', hours:    [ { opens: 'Abre', closes: 'Cierra' } ]},
        {day:'tuesday', hours:    [ { opens: 'Abre', closes: 'Cierra' } ]},
        {day:'wednesday', hours:    [ { opens: 'Abre', closes: 'Cierra' } ]},
        {day:'thursday', hours:    [ { opens: 'Abre', closes: 'Cierra' } ]},
        {day:'friday', hours:    [ { opens: 'Abre', closes: 'Cierra' } ]},
        {day:'saturday', hours:    [ { opens: 'Abre', closes: 'Cierra' } ]},
        {day:'sunday', hours:    [ { opens: 'Abre', closes: 'Cierra' } ]},
    ]);

Updating state on complex data structures gets a little complex because of the immutability issue. As you're discovering, you can't just change the state object directly. You actually have to create a copy of the state, then mutate your copy, then call your setState with the new copy.

But React's setState wants you to provide this new version of your state without changing or mutating the original version. This means that every array or object that contains a change needs to be shallow-copied, all the way from the top down to where you made the change. For a several level deep state like yours, this ends up looking something like this in vanilla JavaScript:

// openingTimes[dayIndex].hours[splitIndex].opens = selectedHour+':'+selectedMinute;
// every object/array containing a change needs to be "cloned" to make it a new object/array.
setOpeningTimes(x => {
  const newState = [...x]; // clone array
  let nextLevel = x[dayIndex];
  newState[dayIndex] = { ...nextLevel }; // clone object
  nextLevel = nextLevel.hours;
  newState[dayIndex].hours = [...nextlevel]; // clone array
  newState[dayIndex].hours.opens = selectedHour + ':' + selectedMinute; // change string value
  return newState; // return iteratively cloned new state.
});

Fortunately there is an npm package calledimmutability-helper which does a reasonably good job of making it easier. (not easy, but easier). With immutability helper, your mutations would look like this:

import update from 'immutability-helper';
// ...

// openingTimes[dayIndex].hours[splitIndex].opens = selectedHour+':'+selectedMinute;
setOpeningTimes(x => update(x, {
  [dayIndex]: {
    hours: {
      [splitIndex]: {
        $set: selectedHour + ':' + selectedMinute
      }
    }
  }
}));

// openingTimes[i].hours.pop();
setOpeningTimes(x => update(x, { [i]: { hours: { $splice: [[x[i].hours.length-1, 1]] } } }));

// openingTimes[i].hours.push({ opens: 'Abre', closes: 'Cierra' });
setOpeningTimes(x => update(x, { [i]: { hours: { $push: { opens: 'Abre', closes: 'Cierra' } } } }));

Definitely a step in the right direction, but all those nested objects means you have a lot of braces to balance. You have a limited selection of $ functions to work with. (eg you'll notice we had to use their $slice , since they don't have a $pop .)

As an add-on to immutability-helper, I've often used this small function to simplify things, because all those nested objects in immutability-helper can get a little tedious IMHO. Using this "update2" helper, you can provide either a string or an array for your "path," and it will build the nested object structure for you.

import update from 'immutability-helper';
// ...
const update2 = (state, path, value) => update(state, toObj(path, value));

function toObj(arr, value) {
  const obj = {}; let o = obj;
  if (typeof arr === 'string') arr = arr.split('.');
  const last = arr.pop();
  while (arr.length) {
    const key = arr.shift();
    if (!o[key]) o[key] = {};
    o = o[key];
  }
  o[last] = value;
  return obj;
}

This is what it looks like it practice. I'll use an array for the first two paths, and then the last path I'll use the string notation.

// openingTimes[dayIndex].hours[splitIndex].opens = selectedHour+':'+selectedMinute;
setOpeningTimes(x => update2(x, [dayIndex, 'hours' splitIndex, 'opens', '$set'], selectedHour+':'+selectedMinute));
// openingTimes[i].hours.pop();
setOpeningTimes(x => update2(x, [i, 'hours', '$splice'], [[x[i].hours.length-1, 1]]));
// openingTimes[i].hours.push({ opens: 'Abre', closes: 'Cierra' });
setOpeningTimes(x => update2(x, `${i}.hours.$push`, { opens: 'Abre', closes: 'Cierra' }));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM