Reputation: 29
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
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
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
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