0

I'm currently working on a new application in React. This is the first time I'm creating something in React. The application will display our own promotions.

My initial state is as follows:

{
  "promotion": {
    "name": "",
    "campaign": "",
    "url": "https://",
    "position": 0,
    "periods": [
      {
        "startDateTimeStamp": 1510558814960,
        "endDateTimeStamp": 1510558814960,
        "variants": [
          {
            "title": "",
            "text": "",
            "image": ""
          }
        ]
      }
    ]
  }
}

This is created from my defaultPromotion constant. This constant is stored in a separate file, which I call api.js

export const defaultPromotion = {
  name: '',
  campaign: '',
  url: 'https://',
  position: 0,
  periods: [
    {
      startDateTimeStamp: Date.now(),
      endDateTimeStamp: Date.now(),
      variants: [
        {
          title: '',
          text: '',
          image: '',
        },
      ]
    },
  ]
}

In my createPromotion component it's created as followed

let promotionState = api.promotions.defaultPromotion;

this.state = {
  promotion: promotionState
};

I can add a new period with the following:

addPromotion() {
    let promotion = this.state.promotion;
    promotion.periods.push( api.promotions.defaultPromotion.periods[0] );
    this.forceUpdate();
}

After that, a new period is added as expected. Suggestions to do this with setState() are very welcome! So, my new state is now:

{
  "promotion": {
    "name": "",
    "campaign": "",
    "url": "https://",
    "position": 0,
    "periods": [
      {
        "startDateTimeStamp": 1510559984421,
        "endDateTimeStamp": 1510559984421,
        "variants": [
          {
            "title": "",
            "text": "",
            "image": ""
          }
        ]
      },
      {
        "startDateTimeStamp": 1510559984421,
        "endDateTimeStamp": 1510559984421,
        "variants": [
          {
            "title": "",
            "text": "",
            "image": ""
          }
        ]
      }
    ]
  }
}

Now, I want to add a new variant for this promotion period, this is where I'm stuck for 2 days now.

I'm adding a new period as follows:

addVariant( periodKey ) {
  const promotion = this.state.promotion;
  promotion.periods[periodKey].variants.push(api.promotions.defaultPromotion.periods[0].variants[0]);
  this.setState({ promotion: promotion });
}

periodKey is here "1", so, I'm expecting that there will be added a new variant for periods[1], but, it's added to both periods. State is now as follows:

{
  "promotion": {
    "name": "",
    "campaign": "",
    "url": "https://",
    "position": 0,
    "periods": [
      {
        "startDateTimeStamp": 1510559984421,
        "endDateTimeStamp": 1510559984421,
        "variants": [
          {
            "title": "",
            "text": "",
            "image": ""
          },
          {
            "title": "",
            "text": "",
            "image": ""
          }
        ]
      },
      {
        "startDateTimeStamp": 1510559984421,
        "endDateTimeStamp": 1510559984421,
        "variants": [
          {
            "title": "",
            "text": "",
            "image": ""
          },
          {
            "title": "",
            "text": "",
            "image": ""
          }
        ]
      }
    ]
  }
}

Can someone explain me why this is happening and how I can add a new variant the right way?

Many, many thanks in advance!

UPDATE 1

Based on the answers from bennygenel and Patrick Hübl-Neschkudla, my implementation is now as follows:

Setting the initial state:

constructor(props) {
      super(props);

      let promotionState = api.promotions.defaultPromotion;
      this.state = { ...promotionState };

}

Method:

addVariant( periodKey ) {
  this.setState((prevState) => {
    const { periods } = prevState;

    periods[periodKey].variants.push(
      Object.assign({}, { ...periods[periodKey].variants, api.promotions.defaultPromotion.periods[0].variants[0]})
    );

    return { periods };
  });
}

But this still is setting the new variant in all the periods. I've also tried the exact code from Benny, but with the same results. The method is called as

this.props.addVariant( this.props.periodKey );

Even when I call it as:

this.props.addVariant(2);

The same behaviour is happening.

UPDATE 2

I now have rewritten everything to redux, this is so I have access to my promotion in every component the easy way, instead off passing them through certain components. Based on the answer of @mersocarlin, I now have the following reducer cases:

Add period

case PROMOTION_ADD_PERIOD:
    const { periods } = { ...state };
    periods.push(api.promotions.defaultPromotion.periods[0]);

    state = {
        ...state,
        periods: periods
    };
    break;

Add a period variant

case PROMOTION_ADD_PERIOD_VARIANT :

  state = {
    ...state,
    periods: [
      ...state.periods[action.payload.period],
      {
        variants: [
          ...state.periods[action.payload.period].variants,
          api.promotions.defaultPromotion.periods[0].variants[0]
        ]
      }
    ]
  };

  break;

The following case: Add a new variant, works, state:

{
  "name": "",
  "campaign": "",
  "url": "https://",
  "position": 0,
  "periods": [
    {
      "startDateTimeStamp": 1510599968588,
      "endDateTimeStamp": 1510599968588,
      "variants": [
        {
          "title": "",
          "text": "",
          "image": ""
        }
      ]
    },
    {
      "startDateTimeStamp": 1510599968594,
      "endDateTimeStamp": 1510599968594,
      "variants": [
        {
          "title": "",
          "text": "",
          "image": ""
        }
      ]
    }
  ]
}

After that, adding a new variant, kinda works, well, the variant is added, but I'm losing my 2nd period. State:

{
  "name": "",
  "campaign": "",
  "url": "https://",
  "position": 0,
  "periods": [
    {
      "variants": [
        {
          "title": "",
          "text": "",
          "image": ""
        },
        {
          "title": "",
          "text": "",
          "image": ""
        }
      ]
    }
  ]
}

I think this is a small thing I'm not see'ing. Does someone have the solution for the "PROMOTION_ADD_PERIOD_VARIANT" case?

Update 3 Changed the "PROMOTION_ADD_PERIOD" case as follows:

case PROMOTION_ADD_PERIOD:
    state = {
        ...state,
        periods: [
            ...state.periods,
            initialState.periods[0]
        ]
    };

    break;

Update 4 Finaly found the solution. See the final code for PROMOTION_ADD_PERIOD_VARIANT below:

state = {
  ...state,
  periods: [
    ...state.periods.map((item, index) => {
      if ( index !== action.payload.period ) {
        return item;
      }

      return {
        ...item,
        variants: [
          ...item.variants,
          initialState.periods[0].variants[0]
        ]
      }
    })
  ]
};

Thank you all so much for your help!!

1
  • Well, you are working on the same reference, your defaultPromotion is also your this.state.promotion. Try to think of which state engine you wish to use (eg redux, or mobx) and check the tutorials how to do it Commented Nov 13, 2017 at 8:23

3 Answers 3

1

Rather destruct your state object and avoid mutating it directly. This also happens to be a bad pattern.

Whenever you need to add a new item to the array:

const state = {
  arrayProp: [{ prop1: 'prop1', prop2: 'prop2' }]
}

const newItem = {
  prop1: 'value1',
  prop2: 'value2',
}

const newState = {
  ...state,
  arrayProp: [
     ...state.arrayProp,
     newItem,
  ]
}

console.log('newState', newState)

Same applies for nested properties within your state: Redux also uses this very same approach

const state = {
  objectProp: {
    arrayPropWithinArray: [
      { id: '0', otherProp: 123, yetAnotherProp: 'test' },
      { id: '1', otherProp: 0, yetAnotherProp: '' }
    ]
  }
}

const { objectProp } = state

const index = objectProp.arrayPropWithinArray.findIndex(obj => obj.id === '1')

const newSubItem = {
  otherProp: 1,
  yetAnotherProp: '2',
}

const newState = {
  ...state,
  objectProp: {
    ...objectProp,
    arrayPropWithinArray: [
      ...objectProp.arrayPropWithinArray.slice(0, index),
      {
        ...objectProp.arrayPropWithinArray[index],
        ...newSubItem,
      },
      ...objectProp.arrayPropWithinArray.slice(index + 1),
    ]
  }
}

console.log('newState', newState)

Your specific case (as described in your comment)

const periodKey = '2' // your periodKey var. Get it from the right place, it can be your action for example
const index = state.periods.findIndex(period => period.id === periodKey) // find which index has to be updated

state = {
  ...state, // propagates current state
  periods: [
    ...state.periods.slice(0, index), // propagates everything before index
    {
      ...state.periods[index],
      variants: [
        ...state.periods[index].variants,
        api.promotions.defaultPromotion.periods[0].variants[0],
      ],
    },
    ...state.periods.slice(0, index + 1) // propagates everything after index
  ]
}
Sign up to request clarification or add additional context in comments.

6 Comments

Hi Merso, Thanks for your answer, I'm almost there :-D. I've rewritten my code to redux, and the adding of periods does work. Also the adding of variants does work, but when I add a new variant in e.g. period[2], then I lose all my periods. See update 2 in my start post.
Hey @Danny, this line is wrong const { periods } = { ...state }. Please try to replace it to: const { periods } = state. With that you pull prop periods from state object.
Hi Merso, Thanks so much! I've edited the "PROMOTION_ADD_PERIOD" case so it's using the spread operator, that piece of code is now as it should be, I think ;-). See "Update 3" in my question. But the problem in the "PROMOTION_ADD_PERIOD_VARIANT" still exists. When adding a new period variant, I'm losing all my periods and the new variant is added to periods[0].
No problem @Danny! We are here to help you :) So, what is PROMOTION_ADD_PERIOD_VARIANT supposed to do? Your state has a periods prop (array) and you want to add a new variant into period at position action.payload.period, right?
The shortest answer is that PROMOTION ADD PERIOD VARIANT should copy the periods[0].variants[0] and add it to for example: state.periods[2].variants, where 2 is ofcourse the periodKey var. That way, state.periods[2].variants should be an array, containing 2 objects. Just like the state in my 2nd update, but without removing all my periods.
|
1

So, what's happening here is that you have an array with two references to the same object.

Imagine it like this:

myArray[0] = reference to defaultPromotion
myArray[1] = reference to defaultPromotion

That's actually a wonderful example of why immutability concepts got so much attention in the past few years :)

What you'd want to do here is instead of adding the defaultPromotion object to the promotions array, you create a new object with the same props as this object and add it. It would look something like this (depending on your ES version etc.)

promotion.periods.push( Object.assign({}, api.promotions.defaultPromotion.periods[0]) );

This way, you're creating a new object and pass this to the array instead of a reference to the already existing one.

2 Comments

Hi Patrick, thanks for your help! I've combined your answer with @bennygenel's answer, for the periods it's working fine. But for the variants, it's still the same: addVariant( periodKey ) { this.setState((prevState) => { const { periods } = prevState; periods[periodKey].variants.push( Object.assign({}, api.promotions.defaultPromotion.periods[0].variants[0]) ); return { periods }; }); } Can you tell me what I'm doing wrong?
That's probably because the variants array is again a reference, you'd need to create a new array here too, either by using a utility library such as lodash lodash.com/docs/4.17.4#cloneDeep or by manually doing it like: [...periods[periodKey].variants, { ...defaultPromotion.periods[0].variants[0] }]
1

First suggestion, if you are going to have only one promotion object in your state and not an array, lose the promotion level. this will reduce the complexity of your state. You can use spread syntax to easily set your initial state.

Example

let promotionState = api.promotions.defaultPromotion;

this.state = { ...promotionState };

Above code would end up creating a state like below;

{
  "name": "",
  "campaign": "",
  "url": "https://",
  "position": 0,
  "periods": [{
    "startDateTimeStamp": 1510559984421,
    "endDateTimeStamp": 1510559984421,
    "variants": [{
      "title": "",
      "text": "",
      "image": ""
    }]
  }, {
    "startDateTimeStamp": 1510559984421,
    "endDateTimeStamp": 1510559984421,
    "variants": [{
      "title": "",
      "text": "",
      "image": ""
    }]
  }]
}

Another suggestion I can make is to use functional setState to reduce possibility to mutate.

Example

addPromotion() {
  this.setState((prevState) => {
    const { periods } = prevState;
    periods.push(api.promotions.defaultPromotion.periods[0]);
    return { periods };
  });    
}

addVariant( periodKey ) {
  this.setState((prevState) => {
    const { periods } = prevState;
    periods[periodKey].variants.push(api.promotions.defaultPromotion.periods[0].variants[0]);
    return { periods };
  });
}

4 Comments

Hi Benny, thank you for your comment. My periods are now correctly handled with setSate, but when adding a new variant with your code, all my periods, still getting the new variant. Do you know what could be possible wrong?
Without seeing your exact implementation I can't say anything. Be sure to not running addVariant for each periodKey. verify the data you are receiving from api. debug your code with console logs.
When I console.log the periodKey in the addVariant function, it does display the correct periodKey, so, it's not being called multiple times. When it was called multiple times, I would expect that I would get 0, 1, 2, 3 etc.
then it should work. Can you add an update to your question with the exact implementation. Maybe someone may add something that we both missing

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.