Lasse Johansen
Lasse Johansen

Reputation: 29

React and state

I would like your take on a specific implementation. I have a react app (no redux), the app has a shopping cart. The shopping cart is defined in the state in the App component and it is passed and used further down the tree in several components. E.g. I have a component called ShoppingCart, it displays the shopping cart, plus it has actions to add/remove/clear the cart.

My problem is updating the shopping cart state after performing an action on the shopping cart. E.g. when I call a function to clear the shopping cart, the state should be updated in the App component thus updating my component which is further down the tree. How would one implement these action functions (without redux)?

Code:

const App = () => {
    const [cart, setCart] = useState({ lines: [], total: 0 });

    return <ShoppingCart cart={cart} />;
}

const ShoppingCart = ({ cart }) => {
    const onAddOne = l => {
        // not sure how to update cart and update state
    }

    const onRemoveOne = l => {
        // not sure how to update cart and update state
    }

    return (
        <table>
            {
                cart.lines.map(l => <tr><td>{l.name}</td><td><button onClick={() => onAddOne(l)}>+</button><button onClick={() => onRemoveOne(l)}>-</button></td></tr>)
            }
        </table>
    );
}

Thanks in advance for any tip.

Upvotes: 1

Views: 120

Answers (3)

thedude
thedude

Reputation: 9814

The straight forward way to implement this is to pass down props to the child component that when called update the state.

Notice how all state business logic is in a central place .e.g in App component. This allows ShoppingCart to be a much simpler.

const App = () => {
  const [cart, setCart] = useState({ lines: [], total: 0 });

  const updateLineAmount = (lineIdx, amount) => {
    // update the amount on a specific line index
    setCart((state) => ({
      ...state,
      lines: state.lines.map((line, idx) => {
        if (idx !== lineIdx) {
          return line;
        }

        return {
          ...line,
          amount: line.amount + amount,
        };
      }),
    }));
  };

  const onAddOne = (lineIdx) => {
    updateLineAmount(lineIdx, 1);
  };

  const onRemoveOne = (lineIdx) => {
    updateLineAmount(lineIdx, -1);
  };

  return (
    <ShoppingCart cart={cart} onAddOne={onAddOne} onRemoveOne={onRemoveOne} />
  );
};

const ShoppingCart = ({ cart, onAddOne, onRemoveOne }) => {
  return (
    <table>
      {cart.lines.map((line, idx) => (
        <tr key={idx}>
          <td>{line.name}</td>
          <td>
            <button onClick={() => onAddOne(idx)}>+</button>
            <button onClick={() => onRemoveOne(idx)}>-</button>
          </td>
        </tr>
      ))}
    </table>
  );
};

Upvotes: 0

Lasse Johansen
Lasse Johansen

Reputation: 29

I found a solution, however, I am not sure it is the correct way to do things:

const App = () => {
    const onUpdateCart = (cart) => {
        setCart({ ...cart });
    }

    const [cart, setCart] = useState({ lines: [], total: 0, onUpdateCart });

    return <ShoppingCart cart={cart} />;
}

const ShoppingCart = ({ cart }) => {
    const onRemoveLine = l => {
        cart.lines = cart.lines.filter(l2 => l2 !== l);
        cart.onUpdateCart(cart);
    }

    const onAddOne = l => {
        l.amount++;
        cart.onUpdateCart(cart);
    }

    const onRemoveOne = l => {
        l.amount--;
        cart.onUpdateCart(cart);
    }

    return (
        <table>
            {
                cart.lines.map(l => (
                    <tr>
                        <td>{l.name}</td>
                        <td>
                            <button onClick={() => onAddOne(l)}>+</button>
                            <button onClick={() => onRemoveOne(l)}>-</button>
                            <button onClick={() => onRemoveLine(l)}>x</button>
                        </td>
                    </tr>)
                )
            }
        </table>
    );
};

Upvotes: 0

bhattaraijay05
bhattaraijay05

Reputation: 492

Here you can use the useContext hook. The idea is similar to redux. So, what you can do is, first create a StateProvider, like in the example

import React, { createContext, useReducer, useContext } from "react";
export const StateContext = createContext();
export const StateProvider = ({ reducer, initialState, children }) => (
  <StateContext.Provider value={useReducer(reducer, initialState)}>
    {children}
  </StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext);

Similarly, create a Reducer for that, you can add more reducers, the example shown is to ADD ITEMS IN BASKET and REMOVE ITEMs FROM BASKET

export const initialState = {
  basket: [],
  user: null,
};

export const getBasketTotal = (basket) =>
  basket?.reduce((amount, item) => item.price + amount, 0);

function reducer(state, action) {
  switch (action.type) {
    case "ADD_TO_BASKET":
      return { ...state, basket: [...state.basket, action.item] };
    case "REMOVE_ITEM":
      let newBasket = [...state.basket];
      const index = state.basket.findIndex(
        (basketItem) => basketItem.id === action.id
      );
      if (index >= 0) {
        newBasket.splice(index, 1);
      } else {
        console.warn("Cant do this");
      }
      return { ...state, basket: newBasket };
    default:
      return state;
  }
}

export default reducer;

Go to your index.js file and wrap your file like this

<StateProvider initialState={initialState} reducer={reducer}>
  <App />
</StateProvider>

And voila, while adding items to the basket use following code

 const addtobasket = () => {
    dispatch({
      type: "ADD_TO_BASKET",
      item: {
        id: id,
        title: title,
        price: price,
        rating: rating,
        color: color,
      },
    });
  };

Upvotes: 1

Related Questions