Joseph
Joseph

Reputation: 7755

Adding an Object to a Nested Array in Redux React

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
      },
    ]
  }
]

Upvotes: 1

Views: 279

Answers (1)

HMR
HMR

Reputation: 39270

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>

Upvotes: 1

Related Questions