The .push() in insertToCart() function in Cart component is pushing twice, no idea why. Since all the logs run once, I think it should just work, the appendToCart() works perfectly. I'm returning the previous value on the functions on purpose, to not trigger a re-render of the whole app. Any help would be much appreciated.
Edit : saveItem() is being called twice.
export default function Cart({ children }) {
const [cart, setCart] = useState([]);
useEffect(() => {
setCart(JSON.parse(localStorage.getItem('frentesRosarinos')) || []);
}, []);
function insertToCart(product, amount) {
console.log('insert');
setCart((pc) => {
pc.push({ ...product, amount });
localStorage.setItem('frentesRosarinos', JSON.stringify(pc));
return pc;
});
}
function appendToCart(id, amount) {
console.log('append');
setCart((pc) => {
const savedItem = pc.find((c) => c.id === id);
savedItem.amount = amount;
localStorage.setItem('frentesRosarinos', JSON.stringify(pc));
return pc;
});
}
return (
<CartContext.Provider
value={{ cart, insertToCart, appendToCart }}
>
{children}
</CartContext.Provider>
);
}
export default function Product({ product, isCart }) {
const { cart, appendToCart, insertToCart } =
useContext(CartContext);
const [addItemClosed, setAddItemClosed] = useState(true);
const [amount, setAmount] = useState(
product.amount || cart.find(({ id }) => id === product.id)?.amount || 0,
);
const amountToAdd = useRef(0);
function saveItem(value) {
setAmount((pa) => {
const newAmount = parseInt(value) + pa;
pa === 0 ? insertToCart(product, newAmount) : appendToCart(product.id, newAmount);
return newAmount;
});
amountToAdd.current.value = 0;
}
return (
<div className={styles.container}>
<div className={styles.addItemsContainer}>
<p
onClick={() => setAddItemClosed((ps) => !ps)}
className={`${styles.addItemsButton} ${addItemClosed ? styles.addBorders : ''}`}
>
Agregar más items
</p>
<div
className={`${styles.quantity} ${
addItemClosed ? styles.addBorders : styles.firstChildHide
}`}
>
<span>Ingrese una cantidad</span>
<input ref={amountToAdd} type='number' />
</div>
<p
className={`${styles.addOne} ${
addItemClosed ? styles.addBorders : styles.secondChildHide
}`}
onClick={() => saveItem(amountToAdd.current.value)}
>
Agregar
</p>
</div>
<div style={{ margin: '10px 0' }}>
<PressButton action={() => saveItem(1)} size='medium' lightMode>
{isCart ? 'Agregar otro' : 'Añadir al carrito'}
</PressButton>
</div>
</div>
);
}
I'm returning the previous value on the functions on purpose, to not
trigger a re-render of the whole app. Any help would be much
appreciated
You should not do this, official react docs say you should update state in immutable way, and you should follow this advice.
Also here:
pc.push({ ...product, amount });
again you are doing mutation which is not OK.
PS. Also in general you should be careful when calling functions inside functional set state in case they perform side effects, because in strict mode the setter function will be invoked twice. Calling a set state within another set state (as you call setCart inside setAmount) could be considered a side effect.
You shouldn't be calling inside a state setter a function that will call this same setter. Change saveItem to this:
function saveItem(value) {
amount === 0 ? insertToCart(product, newAmount) : appendToCart(product.id, newAmount);
setAmount((pa) => {
const newAmount = parseInt(value) + pa;
return newAmount;
});
amountToAdd.current.value = 0;
}
Change insertToCart, so you don't mutate the array (this line pc.push({ ...product, amount }); is a state mutation). Use destructing instead, like so:
function insertToCart(product, amount) {
console.log('insert');
setCart((pc) => {
const newCart = [...pc, { ...product, amount }]
localStorage.setItem('frentesRosarinos', JSON.stringify(newCart));
return newCart;
});
}
Related
I’m running into an error that I could use some help on
Basically, I have a react app that is executing an HTTP call, receiving an array of data, and saving that into a state variable called ‘tasks’. Each object in that array has a key called ‘completed’. I also have a checkbox on the page called ‘Show All’ that toggles another state variable called showAll. The idea is by default all tasks should be shown however if a user toggles this checkbox, only the incomplete tasks (completed==false) should be shown. I can get all tasks to display but can’t get the conditional render to work based on the checkbox click
Here’s how I’m implementing this. I have the HTTP call executed on the page load using a useEffect hook and available to be called as a function from other change handlers (edits etc.)
Before I call the main return function in a functional component, I’m executing a conditional to check the status of ’ShowAll’ and filter the array if it's false. This is resulting in too many re-render errors. Any suggestions on how to fix it?
See simplified Code Below
const MainPage = () => {
const [tasks, setTasks] = useState([]); //tasks
const [showAll, setShowAll] = useState(true); //this is state for the checkbox (show all or just incomplete)
useEffect( ()=> {
axios.get('api/tasks/')
.then( response => { //this is the chained API call
setTasks(response.data.tasks);
})
.catch(err => {
console.log('error');
})
}, []);
const fetchItems = (cat_id) => {
axios.get('/api/tasks/')
.then( response => {
setTasks(response.data.tasks);
})
.catch(err => {
console.log('error');
})
};
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(!showAll)
console.log('Checkbox: ', showAll)
};
//this part updates the tasks to be filtered down to just the incomplete ones based on the checkbox value
if (showAll === false) {
setTasks(tasks.filter(v => v['completed']===false)); //only show incomplete tasks
}
return (
<div>
<label className="checkb">
<input
name="show_all"
id="show_all"
type="checkbox"
checked={showAll}
onChange={handleCheckboxChange}
/> Show all
</label>
<br/>
{ tasks && tasks.map((task, index) => {
return (
<div key={index} className="task-wrapper flex-wrapper">
<div >
{ task.completed === false ? (
<span> {index +1}. {task.task_description} </span> ) :
(<strike> {index +1}. {task.task_description} </strike>) }
</div>
<div>
<button
onClick={()=> modalClick(task)}
className="btn btn-sm btn-outline-warning">Edit</button>
<span> </span>
</div>
</div>
)
})}
</div>
);
};
export default MainPage;
Thanks
Two things to fix:
Use the checked property on event.target to update the state:
const handleCheckboxChange = ({target: { checked }}) => {
setShowAll(checked)
};
Filter as you want but don't update the state right before returning the JSX as that would trigger a rerender and start an infinite loop:
let filteredTasks = tasks;
if (!showAll) {
filteredTasks = tasks?.filter(v => !v.completed));
}
and in the JSX:
{ tasks && tasks.map should be {filteredTasks?.map(...
use e.target.value and useEffect :
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(e.target.checked)
console.log('Checkbox: ', showAll)
if (!e.target.checked) {
let list =tasks.filter(v => v.completed===false);
setTasks(list ); //only show incomplete tasks
}
};
or
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(e.target.checked)
console.log('Checkbox: ', showAll)
};
useEffect(()=>{
if (showAll === false) {
let list =tasks.filter(v => v.completed===false);
setTasks(list ); //only show incomplete tasks
}
},[showAll])
So, I am running into this problem where I am making filters while using the one graphql query. There's a child, parent & grandparent(these are different components). Now my query uses variables inside it, initially on load I set the variables in useState and it's working fine. Now when I click on a checkbox(which is insinde Child component) it passed its data(which is variable for new query) to the Grandparent and I am getting that, so I pass that data into the query variable. But it's not re-redering the query again with new variable. So my filters are not working.
Grand Parent
// handling all the product grid actions and data
function ProductGrid() {
const [queryVariables, setQueryVariables] = useState({first: 20});
console.log(queryVariables);
// get the variable object
const { loading, error, data, fetchMore} = useQuery(QUERY, {
variables: queryVariables
});
if (loading) return <p>Loading...</p>;
if (error) return (
<p>
{console.log(error)}
</p>
);
let productEdges = data.products.edges;
return(
<div className="outContainer">
{/* <PriceFilter/> */}
<TypeFilter getFilters={queryVariables => setQueryVariables(queryVariables)} />
{/* test button */}
<div className="product-grid">
{productEdges.map((element, index) => {
// formatting the price
let tempPrice = Math.floor(element.node.priceRange.minVariantPrice.amount);
let productPrice = new Intl.NumberFormat().format(tempPrice);
return(
<div className="container" key={index}>
<div className="image-container">
<img src={element.node.images.edges[0].node.transformedSrc} alt={element.node.title} />
</div>
<div className="product-title">{element.node.title}</div>
<div>{element.node.priceRange.minVariantPrice.currencyCode}. {productPrice}</div>
</div>
)
})}
</div>
{/* load more products button */}
<button
className="load-more"
onClick={()=>{
const endCursor = data.products.edges[data.products.edges.length - 1].cursor;
fetchMore({
variables: {
after: endCursor,
queryVariables
}
})
}}>
Load More
</button>
</div>
)
}
// graphql query for products fetching
const QUERY = gql`
query productFetch($first:Int, $after:String, $query:String){
products(first:$first, after: $after, query:$query){
edges{
node{
priceRange{
minVariantPrice{
amount
currencyCode
}
}
title
images(first:1){
edges{
node{
transformedSrc(maxWidth: 300)
}
}
}
}
cursor
}
pageInfo{
hasNextPage
}
}
}
`
Parent
// ************** Parent ***************
function TypeFilter(props) {
// assume other code is here for a modal pop and
accordion inside here
// passing the prop to checkbox component here and
getting back new state which we use as a callback for
it's parent
<TypeCheckBox getCheckState={queryVariables =>
props.getFilters(queryVariables)} />
}
Child
// ************** Child *****************
let result = "";
let variables =
{
first: 28,
query: ""
};
function TypeCheckBox(props){
// below function returns variables for apollo query
const handleCheckChange = (event) => {
setState({ ...state, [event.target.name]: event.target.checked });
if(event.target.checked){
// pass this value into the productGrid component
if(counter > 1){
result += "&";
}
result = `${result}product_type:${event.target.value}`;
counter++;
// setting filter type to result
console.log(result);
variables.query = result;
console.log(variables);
return props.getCheckState(variables);
}else{
result = result.replace(`product_type:${event.target.value}`, "");
result = removeLast(result, "&");
counter--;
// setting filter type to result
console.log(`in else ${result}`);
variables.query = result;
console.log(variables);
return props.getCheckState(variables);
}
};
}
return (
<FormGroup column>
<FormControlLabel
control={<Checkbox checked={state.checkedBridal} value="Bridal" onChange={handleCheckChange} name="checkedBridal" />}
label="Bridals"
/>
)
}
I tried using useEffect on useQuery in GrandParent, but then the returned constants don't have access outside, like
useEffect(() => {
const { loading, error, data, fetchMore} = useQuery(QUERY, {
variables: queryVariables
});
}, [queryVariables])
Thanks you soo much for your answers ^^
So, I figured out a solution since no one answered it so I am gonna answer it.
first, you need to add refetch and fetch-policy in useQuery hook as
const { loading, error, data, fetchMore, refetch, networkStatus } = useQuery(
QUERY,
{
variables: queryVariables,
// notifyOnNetworkStatusChange: true,
fetchPolicy: "network-only"
}
);
Then you need to make a separate function with the same name as your props that you're passing to your children and use the spread operator with queryVariables to expand and refetch the query with new variables as
const getFilters = queryVariables => {
setQueryVariables({ ...queryVariables });
refetch();
};
Hope this is helpful to someone ^^
I want to remove object from my list by clicking on delete icon, but with my logic either everything is deleted from list or nothing, I am not sure how to do it without provided ID to object, I don't have anything unique and I am kinda lost.
Component that renders as many Food as there is in useState:
{cartFood.map((food) => {
return (
<CartFood
key={Math.random()}
foodName={food.foodName}
foodPrice={food.foodPrice}
numberOfPortions={food.numberOfPortions}
cartFood={cartFood}
setCartFood={setCartFood}
/>
);
})}
Logic for removing that particular item that is selected (which is not working and also bad solution since there can be case where you get same name and price twice)
const CartFood = ({
foodName,
foodPrice,
numberOfPortions,
cartFood,
setCartFood,
}) => {
const handleRemoveFood = () => {
setCartFood(
cartFood.filter(
(el) =>
el.foodName &&
el.foodPrice !== cartFood.foodName &&
cartFood.foodPrice
)
);
};
return (
<div className='cartFood-container'>
<p>{foodName}</p>
<p>x{numberOfPortions}</p>
<p>{foodPrice}kn</p>
<p>
<MdDeleteForever
className='cartFood__icon'
onClick={handleRemoveFood}
/>
</p>
</div>
);
};
export default CartFood;
List of objects looks like this:
[{
foodName: "Njoki with sos"
foodPrice: 35
numberOfPortions: 1
},
{
foodName: "Chicken Wingos"
foodPrice: 45
numberOfPortions: 2
}]
Put the index of the item in the array as the id. Pass it as your id.
{cartFood.map((food, index) => {
return (
<CartFood
key={index}
id={index}
foodName={food.foodName}
foodPrice={food.foodPrice}
numberOfPortions={food.numberOfPortions}
cartFood={cartFood}
setCartFood={setCartFood}
/>
);
})}
Use the id to remove the food.
const CartFood = ({
foodName,
foodPrice,
numberOfPortions,
cartFood,
setCartFood,
id,
}) => {
const handleRemoveFood = () => {
setCartFood(cartFood.filter((el) => el.id !== id));
};
return (
<div className='cartFood-container'>
<p>{foodName}</p>
<p>x{numberOfPortions}</p>
<p>{foodPrice}kn</p>
<p>
<MdDeleteForever
className='cartFood__icon'
onClick={handleRemoveFood}
/>
</p>
</div>
);
};
Something like this should work :
const handleRemoveFood = (obj) => {
setCartFood((oldList) => oldList.filter((item) => item.foodName !== obj.foodName));
};
Your button (icon) should call this function with current object data (obj)
A working example : https://codesandbox.io/s/cart-isz6c?file=/index.js
From what I see in your repo:
Just pass the food._id to FoodCard so you access it when you want to add or remove an item from cart:
FoodList.js
const foodList = (typeOfList) =>
typeOfList.map(food => {
return (
<FoodCard
key={food._id}
foodId={food._id}
foodName={food.title}
foodPrice={food.price}
foodPic={food.image}
setCartFood={setCartFood}
cartFood={cartFood}
/>
);
});
FoodCard.js
const handleAddToCard = () => {
setCartFood([
...cartFood,
{
foodId,
foodName,
foodPrice,
numberOfPortions,
},
]);
};
CartFood.js
const handleRemoveFood = () => {
setCartFood(cartFood => cartFood.filter((el) => el.foodId !== foodId));
};
Working example:
You could use useReducer with useContext so you don't have to pass props down manually at every level, check this article for more info
You don't need to pass the cartFood as a property just for updating the state since you can use setState callback:
setCartFood(cartFood => [
...cartFood,
{
foodId,
foodName,
foodPrice,
numberOfPortions,
},
]);
So im working on an inventory app, converting all my class components to functional components.. but when i try to pass the inventory value to the child element, it gives me an error of can't set .map on undefined
this is my app component
const App = () => {
const [inventory, setInventory] = useState([]);
const [pointer, setPointer] = useState('')
const addProduct = (item) => {
if(inventory.some(product => product.name === item.name)){
setInventory(
inventory.map(product => {
if(product.name === item.name){
product.quantity += parseInt(item.quantity);
return product;
} return product;
})
)
return;
}
const newItem = {
id: uuid(),
name: item.name,
quantity: parseInt(item.quantity),
unit: item.unit
}
setInventory(
...inventory, newItem
)
}
const updateQuantity = (item)=> {
// this.Modal.current.toggleModal();
setPointer(item.id)
}
const confirmUpdate = (quantity, pointer) => {
setInventory(inventory.map(item => {
if(item.id === pointer){
item.quantity = quantity;
return item;
}
return item;
})
)
}
const deleteItem = (id) => {
setInventory(
inventory.filter(item => item.id !== id)
)
}
return (
<div className="App">
<Header />
<div className="container">
<h1 style={{ width: '100%' }}>Inventory</h1>
<AddItem addProduct={addProduct}/>
<Inventory updateQuantity={updateQuantity} deleteItem={deleteItem} inventory={inventory}> </Inventory>
</div>
<UpdateModal confirmUpdate={confirmUpdate} pointer={pointer}/>
</div>
)
}
child component
const Inventory = props => {
return (props.inventory.map(item => (
<Item
key={item.id}
updateQuantity={props.updateQuantity}
deleteItem={props.deleteItem}
item={item}
/>)))
}
All I want is to pass the inventory value in the app component to the inventory component to map it... but I get the following error
TypeError: props.inventory.map is not a function
I'm sure the answer is simple but I'm stuck in a google wormhole and I can't find the answer...
UPDATE...
The attribute is sent as an object not an array for some reason...
console.log(typeof props.inventory) always returns an object no matter what I do...
I tried a couple of methods...
1-Spreading it out as an array inside the attribute value, [...inventory], raises another error
2- Declaring as a new Array() inside the useState hook, still nothing
3- using Array.from(inventory) inside the attribute call, still nothing..
I am new to react so there must be something I'm missing
You are converting the array to Object here:
setInventory({
...inventory, newItem
})
It must be:
setInventory([
...inventory, newItem
])
So here's what I did wrong...
My hook updating function had a wrong syntax but it was uncaught by react, because apparently the attribute is always passed as an object regardless? I'm not sure..
anyways restructuring my hook function fixed it...
instead of
setInventory(
...inventory, newItem
)
it was
setInventory(inventory =>
[...inventory, newItem]
)
yeah, that solved it..
I tried to use react-window's fixedSizeGrid with react-infinite-loader. As mentioned it's issue, infinite-loader does not support fixedSizeGrid for the infinite load. So i found onItemsRendered override method. Now i am trying to render data with it and load on the scroll. But my data is not loading when i scroll. Here is the snippet of my ThumbGrid component I passed data and fetchMore(graphql), total size of my data from the parent component. Can anyone please help me to solve this.:
/*
* ThumbGrid Component
*
*/
<AutoSizer disableHeight>
{({ width }) => {
const columnCount = Math.floor(width / 175);
return (
<InfiniteLoad
isItemLoaded={isItemLoaded}
itemCount={100}
loadMoreItems={fetchMore}
>
{({ onItemsRendered, ref }: any) => {
const newItemsRendered = (gridData: any) => {
const useOverscanForLoading = true;
const {
visibleRowStartIndex,
visibleRowStopIndex,
visibleColumnStopIndex,
overscanRowStartIndex,
overscanRowStopIndex,
overscanColumnStopIndex
} = gridData;
const endCol =
(useOverscanForLoading || true
? overscanColumnStopIndex
: visibleColumnStopIndex) + 1;
const startRow =
useOverscanForLoading || true
? overscanRowStartIndex
: visibleRowStartIndex;
const endRow =
useOverscanForLoading || true
? overscanRowStopIndex
: visibleRowStopIndex;
const visibleStartIndex = startRow * endCol;
const visibleStopIndex = endRow * endCol;
onItemsRendered({
//call onItemsRendered from InfiniteLoader so it can load more if needed
visibleStartIndex,
visibleStopIndex
});
};
return (
<Grid
width={width}
height={height || 700}
columnWidth={105}
columnCount={columnCount}
itemData={{ list: data, columnCount }}
ref={ref}
innerElementType={innerElementType}
onItemsRendered={newItemsRendered}
rowCount={200}
rowHeight={264}
>
{({columnIndex, rowIndex, data, style}) => {
const { list, columnCount } = data;
const item = list[rowIndex * columnCount + columnIndex];
return item ? <ThumbCard style={style} {...item} /> : null;
}}
</Grid>
);
}}
</InfiniteLoad>
);
}}
</AutoSizer>
And here is my Parent component:
export default function() {
....
function loadMore() {
fetchMore({
variables: {
offset: data.artist.albums.items.length,
limit: 10
},
updateQuery: {....}
});
}
return (<div className="album-cell cell-block">
<ThumbGrid
data={data.artist.albums.items}
height={1200}
total={data.artist.albums.total}
fetchMore={loadMore}
/>
</div>)
}
So i finally achieved with help of Gerrit. I should have pass prop's loadMore function directly to fetchMore method and should pass style into ThumbCard. Also confusing one was i was trying to make a demo with codesandbox, it should show some mock data then it must use states. I passed it down to ThumbGrid so updated data will trigger ThumbGrid to re-render.