using spread operator (...) in mapStateToProps causes stale value to be passed to child component

Viewed 116

I have code like this for my component which is a function component:

useEffect(() => {
  if (errors['update'].error || successes['update'].success) {
    setUpdateInProgress(false);
  }
}, [errors['update'].error, successes['update'].success])

error and success properties are independent of one another, and when either of them is true, I want to call setUpdateInProgress.

in mapStateToProps I have code like this:

const mapStateToProps = (store) => {
  return {
      // other fields
      errors: {...store.prop1.errors, ...store.prop2.errors},
      successes: {...store.prop1.errors, ...store.prop2.errors};
}

These errors and successes objects are the props I need to send to my component in order for the useEffect to work. As is apparent, I want to combine properties from multiple places into one(prop1,prop2, ...).

Problem is, it's not working, and the useEffect is not called. To get it to work, I have to split the props like below:

const mapStateToProps = (store) => {
  return {
      // other fields
      prop1Errors: store.prop1.errors,
      prop2Errors: store.prop2.errors,
      prop1Successes: store.prop1.successes,
      prop2Successes: store.prop2.successes
  }

Why would the spread operator lead to a stale value being set from the state whereas using the state value directly wouldn't?

Edit: For those interested, errors and successes have a structure like this:

errors: {
  create: {
    error: true,
    message: "Error in create"
  },
  update: {
    error: false,
    message: ""
  }
  //... same thing for other actions like fetch, etc
}

//... same thing for successes

As for the reducer code, it's something like this:

function reducer(state, action) {
  if(action === "UPDATE") {
    return {...state, errors: { ...state.errors, update: { message: '', error: false }}, successes: { ...state.successes, update: { message: 'Update successful.', success: true }}}
  }
}
1 Answers

Spread properties in object initializers copy their own enumerable properties from a provided object onto the newly created object. If the spread property already exists in the newly created object, it will be automatically replaced.

In other words, if you have 2 objects: const foo = {a: 1} and const bar = {a: 2, b: 1}. The result of {...foo, ...bar} would be {a: 2, b: 1}. So you can see that the assigned a property was the one from the last spread object.

This is exactly your case. You must consider deep merging since you have a multi-dimensional structure for errors and successes.

Based on your errors structure, I would do a function that merges the errors/successes as follows:

const mergeMessages = (type, ...messages) =>
{
    return messages.reduce((carry, message) => {
        for (let [key, value] of Object.entries(message)) {
            if (!carry.hasOwnProperty(key) || value[type] === true) {
                carry[key] = value;
            }
        }

        return carry;
    }, {});
}

Then you can use it like:

const mapStateToProps = (store) => {
  return {
      // other fields
      errors: mergeMessages('error', store.prop1.errors, store.prop2.errors),
      successes: mergeMessages('success', store.prop1.successes, store.prop2.successes),
  }
}
Related