1

I'm trying to add a new card in the banking_cards array inside of the paymentMethods array. The banking_cards array is inside the paymentMethods array. So i wanted to insert the new card object inside the banking_cards array. My code below produces an error which says state.paymentMethods.banking_cards is not iterable.

TAKE NOTE

banking_cards array is inside the paymentMethods array

export const initialState = {
    paymentMethods: [],
  };



  case paymentMethodConstants.ADD_CARD_SUCCESS:
    return {
      ...state,
      paymentMethods: [
        ...state.paymentMethods,
        banking_cards: [...state.paymentMethods.banking_cards, action.payload],
      ],
    };

JSON

paymentMethods = [
  {
    "id": 8,
    "customer_token": "epofjoe",
    "banking_cards": [
      {
        "id": 1,
        "banking_token": "AAA",
        "last_4": "0006",
        "exp_year": 2021,
        "exp_month": 12,
        "cvc": 876
      },
      {
        "id": 2,
        "banking_token": "BBB",
        "last_4": "0002",
        "exp_year": 2022,
        "exp_month": 12,
        "cvc": 877
      },
    ]
  }
]
8
  • 1
    in the initialState, paymentMethods is an array but in your reducer function, it's an object? because you have paymentMethods: {... Commented Jul 2, 2020 at 7:34
  • @tanmay. paymentMethods should be an array. paymentMethods contains the banking_cards which is also an array. I need to add the new card object inside that of banking_cards Commented Jul 2, 2020 at 7:39
  • Arrays in javascript can't contain string keys...so that banking_cards doesn't mean anything. Add the structure you want to achieve. Commented Jul 2, 2020 at 8:20
  • @StackedQ. Pls see edited question. As you can see there banking_cards is an array inside of paymentMethods array. When i want to add a new card it should be inserted inside of the banking_cards there. Commented Jul 2, 2020 at 8:26
  • @YevgenGorbunkov. Can you rewrite my code? Thanks Commented Jul 2, 2020 at 9:02

1 Answer 1

1

When adding a card to a payment method your action needs to contain what payment method the card needs to be added to.

Below is a working example of how you could do that:

const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;

const createId = ((id) => () => id++)(3);

const initialState = {
  paymentMethods: [
    {
      id: 8,
      banking_cards: [
        {
          id: 1,
        },
        {
          id: 2,
        },
      ],
    },
    {
      id: 9,
      banking_cards: [
        {
          id: 1,
        },
        {
          id: 2,
        },
      ],
    },
  ],
};
//action types
const ADD_CARD_SUCCESS = 'ADD_CARD_SUCCESS';
//action creators
const addCardSuccess = (payementMethodId, card) => ({
  type: ADD_CARD_SUCCESS,
  payload: { payementMethodId, card },
});
const reducer = (state, { type, payload }) => {
  if (type === ADD_CARD_SUCCESS) {
    const { payementMethodId, card } = payload;
    return {
      ...state,
      paymentMethods: state.paymentMethods.map((method) =>
        method.id === payementMethodId
          ? {
              ...method,
              banking_cards: [
                ...method.banking_cards,
                card,
              ],
            }
          : method
      ),
    };
  }
  return state;
};
//selectors
const selectPaymentMethods = (state) =>
  state.paymentMethods;
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(() => (next) => (action) =>
      next(action)
    )
  )
);
//pure component so not re rendering when nothing changed
const Card = React.memo(function Card({ card }) {
  return <li>card: {card.id}</li>;
});
//pure component so it won't re render when nothing changes
const PaymentMethod = React.memo(function PaymentMethod({
  paymentMethod,
}) {
  const dispatch = useDispatch();
  return (
    <li>
      payment method id: {paymentMethod.id}
      <ul>
        {paymentMethod.banking_cards.map((card) => (
          <Card key={card.id} card={card} />
        ))}
      </ul>
      <button
        onClick={() =>
          dispatch(
            //dispatch action with payment method id
            addCardSuccess(paymentMethod.id, {
              //the new card to be added
              id: createId(),
            })
          )
        }
      >
        Add card
      </button>
    </li>
  );
});
const App = () => {
  const paymentMethods = useSelector(selectPaymentMethods);
  return (
    <ul>
      {paymentMethods.map((paymentMethod) => (
        <PaymentMethod
          key={paymentMethod.id}
          paymentMethod={paymentMethod}
        />
      ))}
    </ul>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/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.2.0/react-redux.min.js"></script>
<div id="root"></div>

With immer I'd use payment method index and not need the map:

const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { produce } = immer;

const createId = ((id) => () => id++)(3);

const initialState = {
  paymentMethods: [
    {
      id: 8,
      banking_cards: [
        {
          id: 1,
        },
        {
          id: 2,
        },
      ],
    },
    {
      id: 9,
      banking_cards: [
        {
          id: 1,
        },
        {
          id: 2,
        },
      ],
    },
  ],
};
//action types
const ADD_CARD_SUCCESS = 'ADD_CARD_SUCCESS';
//action creators
const addCardSuccess = (index, card) => ({
  type: ADD_CARD_SUCCESS,
  payload: { index, card },
});
const reducer = (state, { type, payload }) => {
  if (type === ADD_CARD_SUCCESS) {
    //lot less hassle using index and immer
    const { index, card } = payload;
    return produce(state, (draft) => {
      draft.paymentMethods[index].banking_cards.push(card);
    });
  }
  return state;
};
//selectors
const selectPaymentMethods = (state) =>
  state.paymentMethods;
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(() => (next) => (action) =>
      next(action)
    )
  )
);
//pure component so not re rendering when nothing changed
const Card = React.memo(function Card({ card }) {
  return <li>card: {card.id}</li>;
});
//pure component so it won't re render when nothing changes
const PaymentMethod = React.memo(function PaymentMethod({
  paymentMethod,
  index,
}) {
  const dispatch = useDispatch();
  return (
    <li>
      payment method id: {paymentMethod.id}
      <ul>
        {paymentMethod.banking_cards.map((card) => (
          <Card key={card.id} card={card} />
        ))}
      </ul>
      <button
        onClick={() =>
          dispatch(
            //dispatch action with payment method index
            addCardSuccess(index, {
              //the new card to be added
              id: createId(),
            })
          )
        }
      >
        Add card
      </button>
    </li>
  );
});
const App = () => {
  const paymentMethods = useSelector(selectPaymentMethods);
  return (
    <ul>
      {paymentMethods.map((paymentMethod, index) => (
        <PaymentMethod
          key={paymentMethod.id}
          paymentMethod={paymentMethod}
          index={index}
        />
      ))}
    </ul>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/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.2.0/react-redux.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/immer.umd.production.min.js"></script>
<div id="root"></div>

Sign up to request clarification or add additional context in comments.

3 Comments

Pls remove using immer. Thanks
@Robert See comment below, you can do ? {...method,banking_cards:[...method.banking_cards,card]} instead of immer. If immer was a given I'd use payment method index and the reducer would be much simpler (no map needed). Immer is going to be the thing for now as it's included in the redux template
Thanks. Can you separate code for using immer and not using immer. Separate them into two files not just comment out so i can better understand. Thank you.

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.