Search code examples
javascriptreactjsecmascript-6reduximmutability

Multiple immutable changes on state arrays


I have the following currentState. When i add the recipient, the new recipient should be appended to recipient.allIds and recipientProduct with default qty = 1. I am stuck at appending to the recipientProduct part. This should be done in immutable way as it is a redux state.

let currentState = {
  product: {
    allIds: ['1', '5'],
  },
  recipient: {
    allIds: ['1'],
  },
  recipientProduct: [{
        recipientId: '1',
        productId: '1',
        qty: '3'
    }, {
        recipientId: '1',
        productId: '5',
        qty: '3'
    }],
};

When I append a new recipient:

addRecipient = (recipientId) => {
    // when recipientId = 2
    // note quantity is always default to 1

    -> append productIds to product.allIds if not exists
    -> loop through all product from product.allIds
         loop through all recipient from recipient.allIds
         if (recipientId === recipientId && productId === productId) {
              remain
          } else {
            // append with default qty 1
        }

}

This will be the newState:

newstate = {
    product: {
        allIds: ['1', '5'],
    },
    recipient: {
        allIds: ['1', '2'],
    },
    recipientProduct: [{
        recipientId: '1',
        productId: '1',
        qty: '3'
    }, {
        recipientId: '1',
        productId: '5',
        qty: '3'
    },{
        recipientId: '2',
        productId: '1',
        qty: '1'
    }, {
        recipientId: '2',
        productId: '5',
        qty: '1'
    }],
}

Solution

  • I think it is a reducer that should handle the logic being described, not an action creator (?) addRecipient.

    So, as I understood, in common Redux workflow you need to modify global state properties recipient (new recipient should be pushed in) and recipientProduct (items that correspond to each product with default quantity of 1 should be appended) upon dispatching APPEND_RECIPIENT action or such.

    My solution for that would be the following:

    appReducer = (state=initialState, action) => {
            switch(action.type){
              case 'APPEND_RECIPIENT' : {
                let {payload: recipientId} = action,
                      {recipient:{allIds:recipientIds}, recipientProduct, product:{allIds:productIds}} = state
                if(recipientIds.includes(recipientId)) return state
                recipientIds = [...recipientIds, recipientId]
                recipientProduct = [...recipientProduct,...productIds.map(productId => ({productId, recipientId, qty: '1'}))]
                return {...state, recipient:{allIds:recipientIds}, recipientProduct}
              }
              default: return state
            }
          }
    

    You may find the live-demo of that concept below:

    //dependencies
    const { useState } = React,
          { render } = ReactDOM,
          { createStore } = Redux,
          { connect, Provider } = ReactRedux
    
    //initial state, reducer and store
    const initialState = {product:{allIds:['1','5']},recipient:{allIds:['1']},recipientProduct:[{recipientId:'1',productId:'1',qty:'3'},{recipientId:'1',productId:'5',qty:'3'}]},
          appReducer = (state=initialState, action) => {
            switch(action.type){
              case 'APPEND_RECIPIENT' : {
                let {payload: recipientId} = action,
                      {recipient:{allIds:recipientIds}, recipientProduct, product:{allIds:productIds}} = state
                if(recipientIds.includes(recipientId)) return state
                recipientIds = [...recipientIds, recipientId]
                recipientProduct = [...recipientProduct,...productIds.map(productId => ({productId, recipientId, qty: '1'}))]
                return {...state, recipient:{allIds:recipientIds}, recipientProduct}
              }
              default: return state
            }
          },
          store = createStore(appReducer)
          
    //append recipient form ui component
    const AppendRecipient = ({onAppendRecipient, onInput}) => {
      const [inputValue, setInput] = useState()
      return (
        <form onSubmit={e => (e.preventDefault(), onAppendRecipient(inputValue))}>
          <input type="number" onKeyUp={e => setInput(e.target.value)} />
          <input type="submit" name="Append Recipient" />
        </form>
      )
    }
    //connect onAppendRecipient handler to dispatching 'APPEND_RECIPIENT' action
    const mapDispatchToProps = dispatch => ({
            onAppendRecipient: recipientId => dispatch({type:'APPEND_RECIPIENT', payload:recipientId})
          }),
          AppendRecipientContainer = connect(null, mapDispatchToProps)(AppendRecipient)
    //mirroring recipientProducts
    const RecipientProducts = ({products, productsQty}) => (
      <div>
        {
          products.map(({recipientId,productId,qty},key) => <div {...{key}}>recipient:{recipientId}, product:{productId}, qty: {qty}</div>)
        }
      </div>
    )
    //connect output element to global recipientProducts
    const mapStateToProps = ({recipientProduct}) => ({products:recipientProduct, productsQty:recipientProduct.length}),
          RecipientProductsContainer = connect(mapStateToProps)(RecipientProducts)
          
    //render the entire app
    render (
      <Provider store={store}>
        <AppendRecipientContainer />
        <RecipientProductsContainer />
      </Provider>,
      document.getElementById('root')
    )
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.1.3/react-redux.min.js"></script><div id="root"></div>