I`m trying to use a reducer function to update state. I have a state variable, phones, which is an array of phone objects. When the user clicks a button, I want the quantity property of the phone object to increase. Quantity does get increased, but there's no change in the UI.
Here`s my code:
const initialState = {
phones: [...Phones]
};
const reducer = (state, action) => {
if (action.type === "increase") {
const newPhones = state.phones.map(phone => {
if (phone.id === action.payload) {
return { ...phone, quantity: phone.quantity + 1 };
}
return phone;
});
return { ...state, phones: newPhones };
}
};
const Cart = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div className="cart_div">
<h1>Your Bag</h1>
{Phones.map(phone =>
<PhoneListItem
key={phone.id}
dispatch={dispatch}
state={state}
{...phone}
/>
)}
</div>
);
};
const PhoneListItem = ({ name, price, img, quantity, dispatch, id, state}) => {
return (
<div className="phone_row">
<img className="phone_img" src={img} />
<div className="phone_info_div">
<h4>
{name}
</h4>
<h4 className="price">
{price}
</h4>
</div>
<div className="phone_amount_div">
//user clicks on button to increase phone quantity
<button className="increase_button" onClick={() => dispatch({ type: "increase", payload: id })}>
<Icon icon="akar-icons:chevron-up" />
</button>
<p className="phone_amount_para">{quantity}</p>
<button className="decrease_button" onClick={() => dispatch({ type: "decrease", payload: id })}>
<Icon icon="akar-icons:chevron-down" />
</button>
</div>
</div>
);
};
useReducer will return the whole state instead of a state slice.
You already get the state by array destructuring, but you didn't use it.
const [state, dispatch] = useReducer(reducer, initialState);
{state.Phones.map(phone => {
return <PhoneListItem
key={phone.id}
dispatch={dispatch}
state={state}
{...phone}
/>
})}
Related
I try to do Todo App, I stack on add existing pending task to completed task. How can i add existing item to new state in reducer or there is a another way to do that?
When i click the button which has completed id, i want to push that task to completed state.
Main:
const App = () => {
const [state, dispatch] = useReducer(todoAppReducer, [])
return (
<TodoAppContext.Provider value={{
state: state,
dispatch,
}}>
<BrowserRouter>
<Header/>
<AddTask />
<Sections />
<Routes>
<Route path='' element={ <Pending /> }/>
<Route path='completed' element={ <Completed /> }/>
<Route path='deleted' element={ <Deleted /> }/>
</Routes>
</BrowserRouter>
</TodoAppContext.Provider>
)
}
export default App
Add Task:
const AddTask = () => {
const {dispatch} = useContext(TodoAppContext)
const [task, setTask] = useState("");
const [taskDetail, setTaskDetail] = useState("");
const [error, setError] = useState("")
const onAddTaskSubmit = (e) => {
e.preventDefault();
if(task == "") {
setError("Please enter task!")
} else {
dispatch({
type: 'ADD_PENDING',
task,
taskDetail
})
setError("")
}
setTask("")
setTaskDetail("")
}
return (
<div className='non-bootstrap-container add-task-form'>
<form onSubmit={onAddTaskSubmit}>
<input value={task} onChange={(e) => setTask(e.target.value)} type="text" placeholder='Enter Task'/>
<textarea value={taskDetail} onChange={(e) => setTaskDetail(e.target.value)} placeholder='Task Details (Optional)'></textarea>
<div className='add-button-and-error'>
<p className='error'>{error}</p>
<button type='submit' className='btn btn-primary'>Add Task</button>
</div>
</form>
</div>
)
}
export default AddTask
Reducer
const todoAppReducer = (state, action) => {
switch(action.type) {
case "ADD_PENDING":
return [
...state,
{title: action.task, detail: action.taskDetail}
]
default:
return state
}
}
export default todoAppReducer
When i click the button which has completed id, i want to push that task to completed state.
const Pending = () => {
const {state} = useContext(TodoAppContext)
return (
<div className='non-bootstrap-container tasks'>
{
<ul>
{
state.map((task,index) => (
<li key={index}>
<div>
<span>{index}. </span>
<span>{task.title}</span>
</div>
<span>{task.detail}</span>
<div>
<button className='btn btn-primary' id='completed'><i className="fa-solid fa-check"></i></button>
<button className='btn btn-danger'><i className="fa-solid fa-trash-can"></i></button>
</div>
</li>
))
}
</ul>
}
</div>
)
}
export default Pending
You first need to set the case in the reducer.
Reducer
case "COMPLETED":
return [
...state,
{title: action.task, detail: action.taskDetail}
]
Now try calling it in submit button when the task is completed with dispatch.
Hope it helps! =)
I need to update the state on main page, the problem is that I update values 3 levels down...
This component is where I get all the cities, putting the data on the State and map through cities.
CitiesPage.tsx
export const CitiesPage = () => {
const [cities, setCities] = useState<City[]>([]);
useEffect(() => {
getCities().then(setCities);
}, []);
return (
<>
<PageTitle title="Cities" />
<StyledCitySection>
<div className="headings">
<p>Name</p>
<p>Iso Code</p>
<p>Country</p>
<span style={{ width: "50px" }}></span>
</div>
<div className="cities">
{cities.map((city) => {
return <CityCard key={city.id} city={city} />;
})}
</div>
</StyledCitySection>
</>
);
};
On the next component, I show cities and options to show modals for delete and update.
CityCard.tsx.
export const CityCard = ({ city }: CityProps) => {
const [showModal, setShowModal] = useState(false);
const handleModal = () => {
setShowModal(true);
};
const handleClose = () => {
setShowModal(false);
};
return (
<>
{showModal && (
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} />
</ModalPortal>
)}
<StyledCityCard>
<p className="name">{city.name}</p>
<p className="isoCode">{city.isoCode}</p>
<p className="country">{city.country?.name}</p>
<div className="options">
<span className="edit">
<FiEdit size={18} onClick={handleModal} />
</span>
<span className="delete">
<AiOutlineDelete size={20} />
</span>
</div>
</StyledCityCard>
</>
);
};
and finally, third levels down, I have this component.
EditCityForm.tsx.
export const EditCityForm = ({ city, closeModal }: Props) => {
const [updateCity, setUpdateCity] = useState<UpdateCity>({
countryId: "",
isoCode: "",
name: "",
});
const handleChange = (
evt: ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { target } = evt;
setUpdateCity({ ...updateCity, [target.name]: target.value });
};
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
updateCityHelper(cityId, updateCity);
closeModal(false);
};
useEffect(() => {
setUpdateCity({
isoCode: city.isoCode,
name: city.name,
countryId: city.country?.id,
});
}, [city]);
return (
<form onSubmit={handleSubmit}>
<Input
label="Iso Code"
type="text"
placeholder="Type a IsoCode..."
onChange={handleChange}
name="isoCode"
value={updateCity.isoCode}
/>
<Input
label="Name"
type="text"
placeholder="Paste a Name.."
onChange={handleChange}
name="name"
value={updateCity.name}
/>
<CountrySelect
label="Country"
onChange={handleChange}
value={city.country?.name || ""}
name="countryId"
/>
<Button type="submit" color="green" text="Update" />
</form>
);
};
Edit form where retrieve data passed from CityCard.tsx and update State, passing data to a function that update Info, closed modal and... this is where I don't know what to do.
How can I show the info updated on CitiesPage.tsx when I submitted on EditCityForm.tsx
Any help will be appreciated.
Thanks!
You are storing the updated value in a different state, namely the updateCity state, but what you should be doing is update the origional cities state. While these two states are not related, and at the same time your UI is depend on cities state's data, so if you wish to update UI, what you need to do is update cities' state by using it's setter function setCities.
Just like passing down state, you pass it's setters as well, and use the setter function to update state's value:
// CitiesPage
{cities.map((city) => {
return <CityCard key={city.id} city={city} setCities={setCities} />;
})}
// CityCard
export const CityCard = ({ city, setCities }: CityProps) => {
// ...
return (
// ...
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} setCities={setCities} />
</ModalPortal>
// ...
)
}
// EditCityForm
export const EditCityForm = ({ city, closeModal, setCities }: Props) => {
// const [updateCity, setUpdateCity] = useState<UpdateCity>({ // no need for this
// countryId: "",
// isoCode: "",
// name: "",
// });
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
setCities(); // your update logic
closeModal(false);
};
}
I'd advise you to use React-Redux or Context API whenever you have nested structures and want to access data throughout your app.
However in this case you can pass setCities and cities as a prop to CityCard and then pass this same prop in the EditCityForm component and you can do something like this in your handleSubmit.
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
let updatedCities = [...cities];
updatedCities.forEach(el => {
if(el.id == updateCity.id) {
el.countryCode = updateCity.countryCode;
el.name = updateCity.name;
el.isoCode = updateCity.isoCode;
}
})
setCities(updatedCities);
closeModal(false);
};
I am still pretty new and am doing a simple food ordering app in React. I have a file of dummy meals and am able to create a cart through user input, create a modal on the "Your Cart" button from the header, and map through the cart displaying as a list, but I am unable to figure out how to delete an item from the cart. I realize I need to target the id of the list item that the X is connected to, but for some reason I can't get the this command to work.
Is there something about the this command that I am not understanding when it comes to a mapped list item? So far I have both tried this through my CartModal component and the Home component, but both turn up undefined when using this and I throw an error when using e.target (enter different options like e.target.value, e.target.id, etc...), and have even tried working on an inline targeting (which is where the code below is at currently) but still comes up nothing.
I will add a couple dummy meals to this as well.
Thanks so much!
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "reset":
return initialState;
default:
return state;
}
};
export const CountContext = React.createContext();
const Home = (props) => {
const [cart, setCart] = useState([]);
const [count, dispatch] = useReducer(reducer, initialState);
const {isVisible, toggleModal} = useModal();
let totalPrice;
let cartCounter ;
const cartUpdaterHandler = (cartItem) =>{
setCart([...cart, cartItem]);
cartItem= cartItem;
}
useEffect(() => {
cartCounter = cart.length;
totalPrice = cart.reduce((acc, {price}) => parseFloat(acc) + parseFloat(price), 0).toFixed(2);
}, [cart])
const modalHandler = () => {
toggleModal(true)
}
const removeCI = () => {
console.log(cart)
}
return (
<CountContext.Provider
value={{ countState: count,
countDispatch: dispatch,
currentCart: cart
}}
className={classes.contextProvider}
>
{isVisible && (
<CartModal
overlayClassName="custom_overlay"
isVisible={isVisible}
hideModal={toggleModal}
currentCart={cart}
totalPrice={totalPrice}
remove={removeCI}
/>
)}
<Header cart={cart} cartCounter={cart} modal={modalHandler}/>
<Intro />
<MealForm onAdd={cartUpdaterHandler} />
{/* <Cart /> */}
{/* <CartModal isVisible={isVisible} hideModal={toggleModal} currentCart={cart} totalPrice={totalPrice}/> */}
</CountContext.Provider>
)
}
export default Home;
const CartModal = ({ isVisible, hideModal, currentCart, remove}) => {
// const checkKey = () => {
// console.log(cartItem.id)
// console.log(this.id)
// }
return isVisible
? createPortal(
<Card className={classes.modal}>
<div className={classes.modalBackground}>
<div className={classes.modalDiv}>
<div className={classes.modalHeader}>
<h5 className={classes.modalHeaderH5}>Your Cart</h5>
<button className={classes.modalHeaderX} onClick={hideModal}> X </button>
</div>
<div className={classes.modalBody}>
{currentCart.map((cartItem, i) => {
return<div key={Math.random()}>
<ul className={classes.cartModalItem} key={Math.random()}>
<div className={classes.cartModalItemInfo} >
<li className={classes.cartModalName}>{cartItem.name}</li>
<li className={classes.cartModalPrice}>{cartItem.price}</li>
<li className={classes.cartModalDesc}>{cartItem.description}</li>
</div>
//I am asking about the X below this.
<button className={classes.cartModalX} onClick={()=>{console.log(this)}}>X</button>
</ul>
</div>
})}
<h5 className={classes.modalTotal}>Total:{currentCart.reduce((acc, {price}) => parseFloat(acc) + parseFloat(price), 0).toFixed(2)} </h5>
</div>
<div className={classes.modalFooter}>
<button className={classes.closeModalButton} onClick={hideModal}> Close </button>
<button className={classes.orderModalButton}>Checkout</button>
</div>
</div>
</div>
</Card>,
document.body,
)
: null;
};
export default CartModal;
const AvalibleMeals = [
{
id: 'm1',
name: 'Sushi',
description: 'Finest fish and veggies',
price: 22.99,
},
{
id: 'm2',
name: 'Schnitzel',
description: 'A german specialty!',
price: 16.5,
},
{
id: 'm3',
name: 'Barbecue Burger',
description: 'American, raw, meaty',
price: 12.99,
},
{
id: 'm4',
name: 'Green Bowl',
description: 'Healthy...and green...',
price: 18.99,
},
];
export default AvalibleMeals;
I have a display of products with each name, price and button(add to cart).
The problem is the first data(hoodie white) from my array of products (store component)is the only one that adds up. I can't find a way to identify each array of products when using my handler for dispatch.
Array products sample
const data = {
products:[
{
id: 1,
name: 'Hoodie white',
price: 19.99,
},
{
id: 2,
name: 'Hoodie black',
price: 19.99,
}
],
cart: [],
total: 0,
}
const ProductComponent=()=>{
const products= useSelector(state=>state.products);
const dispatch = useDispatch(0);
const [productid] = useState(0)
const purchaseHandler=(e)=>{
dispatch({ type: "PURCHASE", payLoad: products[productid] });
}
return(
products.map((product, i)=>{
return(
<>
<img src={products.image} className="product-image" alt={product.name} />
<div className='namebtn'>
<h3 className='product-name-text' key={i}>{product.name}</h3>
<div key={i}>${product.price}</div>
<button onClick={purchaseHandler} className="addToCart">Add to Cart</button>
</div>
</>
)})
)
}
Why not pass the product.id to purchaseHandler function
const ProductComponent = () => {
const products = useSelector((state) => state.products);
const dispatch = useDispatch(0);
const purchaseHandler = (e, productid) => {
dispatch({ type: 'PURCHASE', payLoad: products[productid] });
};
return products.map((product, i) => {
return (
<>
<img src={products.image} className="product-image" alt={product.name} />
<div className="namebtn">
<h3 className="product-name-text" key={i}>
{product.name}
</h3>
<div key={i}>${product.price}</div>
<button onClick={(e) => purchaseHandler(e, product.id)} className="addToCart">
Add to Cart
</button>
</div>
</>
);
});
};
I'm having trouble accessing the state in one of my components. I have a component where a user adds a name and a weight and then submits it. They are then redirected to the Home Page. What I want to happen is for the name that was inputed to be displayed on the Home Page. I can see the state updating, but I can't figure out how to access the state and have the name show on the Home Page. Any help would be appreciated.
Here is my Home Page component:
const HomePage = () => {
const classes = useStyles();
const name = useSelector(state => state.move.name);
const displayMovementButtons = () => {
if (name) {
return (
<Button
className={classes.movementButtons}
onClick={() => history.push('/movement/:id')}
>
<div className={classes.movementName} >{name}</div>
</Button>
)
};
return <div className={classes.noMovementsMessage} >Click add button to begin</div>
};
return (
<div className={classes.homePageContent} >
<Header title={"Home Page" }/>
<div>{displayMovementButtons()}</div>
<div className={classes.fabDiv}>
<Fab
className={classes.fab}
onClick={() => history.push(`/add`)}>
<AddIcon />
</Fab>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return {
name: state.move.name,
}
};
const withConnect = connect(
mapStateToProps,
);
export default compose(withConnect)(HomePage);
Here is my reducer, where I think the problem is:
const initialState = []
const addMovementReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_MOVEMENT:
return [ ...state, {name: action.name, weight: action.weight} ]
default:
return state;
}
};
export default addMovementReducer;
Here is a screenshot showing the state (note: I added multiple names and weights, I would eventually like each 'name' to appear on the Home Page):
Your move branch of state is an array. You can't access the name by state.move.name. Instead of this you can get an array of movements from redux store and render them with Array.map() method.
const MovementButtons = ({ movements }) => {
return (
<div>
{
movements.map(({ name, weight }) => {
if (name) {
<Button
className={classes.movementButtons}
onClick={() => history.push('/movement/:id')}
key={name}
>
<div className={classes.movementName}>{name}</div>
</Button>
}
return (
<div className={classes.noMovementsMessage}>Click add button to begin</div>
)
})
}
</div>
);
}
const HomePage = () => {
const classes = useStyles();
const movements = useSelector(state => state.move);
return (
<div className={classes.homePageContent} >
<Header title={"Home Page" }/>
<MovementButtons movements={movements} />
<div className={classes.fabDiv}>
<Fab
className={classes.fab}
onClick={() => history.push(`/add`)}>
<AddIcon />
</Fab>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return {
name: state.move.name,
}
};
const withConnect = connect(
mapStateToProps,
);