I've made a shorter version of what I'm trying to create for simplicity here. I know some of the code here is wrong. I've spent hours trying different ways and can't get anything to work, so I've stripped it back.
Aim: I'm rendering a FlatList. I need to be able to update the 'qty' in each respective object in the array with the click of a button in each particular FlatList item.
So, if I click 'Increase QTY' in 'abc', then the 'qty' data will be increased by 1.
I've looked everywhere online and can't seem to be able to get any closer. Any help would be hugely appreciated.
import React, { useState } from 'React';
import { View, Text, Button, FlatList } from 'react-native';
const DataApp = () => {
const [data, setData] = useState([
{ id: 1, name: 'abc', qty: 1 },
{ id: 2, name: 'def', qty: 2 },
{ id: 3, name: 'ghi', qty: 3 },
]);
const incQuantityHandler = (data) => {
setData([...data, prevState => qty[prevState] + 1 ])
}
const Item = ({ item }) => (
<View>
<Text>{item.name}</Text>
<Text>{item.qty}</Text>
<Button title="Increase QTY" onPress={incQuantityHandler}/>
</View>
)
const renderItem = ({ item }) => (
<Item name={item.name} qty={item.qty} />
)
return (
<View>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
)
}
export default DataApp;
With your current setup, incQuantityHandler gets called with an event object whenever the button is pressed. You should use onClick by the way.
You can use an arrow function to pass on which button you're pressing, otherwise use a wrapper function:
onClick={() => incQuantityHandler(item.name)}
// or alternatively, but basically the same:
const wrapHandler = item => (() => incQuantityHandler(item.name));
onClick={wrapHandler(item)}
Your incQuantityHandler itself is incorrect. I suggest re-reading the React documentation and learning about array destructuring/spreading, but you probably want something like:
// Remember that now we get the item name instead
const incQuantityHandler = (itemName) => {
// Use an arrow function to mutate data
setData(data =>
// Use map to map over all items
data.map(item => {
// Leave other items the way they are
if (item.name !== itemName) return item;
// Return a modified copy of our target item
// where we changed the qty field
return { ...item, qty: item.qty + 1 };
}));
}
Related
While trying to build a React Component that renders a list of objects I ran into some behaviour that I cannot explain.
I use the useState hook to keep track of a list of animals.
When I click the button 'add animal', I add an object to that list containing a random animal.
I do this several times.
...so far so good, an object is created and added every time, and my animalList is properly rendered.
But here is where I get lost;
When I click the 'Remove' button on an Animal component it logs the animalList to the console, but it shows a different value for every item in the list, while I expect it to be the same for all of them.
It appears that the value of animalList is equal to what it was at the time the object was created rather than referencing the state.
My next step would be to remove the clicked object, but they don't seem to share the same reference to the list.
Can somebody help me understand what is happening here? I have added the code required to recreate the issue:
import { useState } from 'react';
import './App.css';
export default function App() {
const [animalList, updateAnimalList] = useState([]);
const animals = [
{name: 'anaconda'}, {name: 'brachiosaurus'}, {name: 'chimpansee'}, {name: 'dragon'}, {name: 'eagle'}, {name: 'fox'},
{name: 'giraffe'}, {name: 'hellhound'}, {name: 'iguana'}, {name: 'jackal'}, {name: 'koala'}, {name: 'lion'},
{name: 'meerkat'}, {name: 'nyan-cat'}, {name: 'ostrich'}, {name: 'pterodactyl'}, {name: 'quail'}, {name: 'rhinoceros'},
{name: 'sfinx'}, {name: 'triceratops'}, {name: 'unicorn'}, {name: 'vampire deer'}, {name: 'whale'}, {name: 'xiao'},
{name: 'yoghurt'}, {name: 'zebra'},
];
const addAnimal = () => {
updateAnimalList([...animalList,
{ ...animals[Math.floor(Math.random() * animals.length)],
onClick: removeAnimal,
}
]);
}
const removeAnimal = () => {
console.log(animalList);
// let newArray = [...animalList];
//newArray.splice(index, 1);
//updateAnimalList(animalList);
};
return (
<div className="app">
<button onClick={addAnimal}>addAnimal</button>
{ animalList.map( (animal, index) => {
return (
<Animal key={index} {...animal} />
)
})}
</div>
);
};
export function Animal(animal) {
return (
<div className="card">
<h2>{ animal.name }</h2>
<button onClick={animal.onClick}>Remove</button>
</div>
)
}
I would avoid storing onClick in animalList because removeAnimal captures a stale reference to a version of animalList.
updateAnimalList can be used like you did or it can also accept an updater function which receives the current value of animalList.
So by combining these two, I would end up with something like this:
const animals = [
// the animals (should be defined outside of the component as it is not changing)
];
export default function App() {
const [animalList, updateAnimalList] = useState([]);
const addAnimal = () => {
updateAnimalList([
...animalList,
animals[Math.floor(Math.random() * animals.length)]
]);
}
const makeRemoveAnimal = (index) => () => {
updateAnimalList((current) => [...current].splice(index, 1))
};
return (
<div className="app">
<button onClick={addAnimal}>addAnimal</button>
{animalList.map((animalName, index) => {
return (
<Animal key={index} name={animalName} onClick={makeRemoveAnimal(index)} />
)
})}
</div>
);
};
Because every time you add an animal here:
{ ...animals[Math.floor(Math.random() * animals.length)],
onClick: removeAnimal,
}
you are also storing a function reference (removeAnimal) in the state.
The version of removeAnimal which you are storing is from the render when the click happened (aka stale closure). Hence, inside removeAnimal:
let newArray = [...animalList];
the animalList is also from the render when the click happened.
No reason to store removeAnimal on each click inside array. Just declare it as function and pass an id of object you want to delete. Then you can always use that single function.
Also you seem to be using index as key which is not recommended especially if array items may reorder. Use some id instead.
So you could do:
{
animalList.map((animal) => {
return (
<Animal
key={animal.id}
{...animal}
onClick={() => removeAnimal(animal.id)}
/>
);
});
}
Then
const removeAnimal = (id) => {
updateAnimalList(animalList.filter((x) => x.id != id));
};
It is not good to store link onClick in animalList because link becomes unactual.
It looks like you should send name of animal. When you will send name of animal, then it would be pretty simple to remove item of array:
// you need to send here name of animal
const removeAnimal = () => {
const updatedArray = animalList.filter( p=> p.name !== 'anaconda')
updateAnimalList(prevAnimals => [...updatedArray]);
};
You can get the same function with useRef
const [animalList, updateAnimalList] = useState([]);
const refRemoveFunction = useRef(() => {});
const addAnimal = () => {
updateAnimalList([
...animalList,
{
...animals[Math.floor(Math.random() * animals.length)],
onClick: refRemoveFunction
}
]);
};
useEffect(() => {
refRemoveFunction.current = (index) => {
animalList.splice(index, 1);
updateAnimalList([...animalList]);
};
}, [animalList]);
And you can use like this:
export function Animal(animal) {
return (
<div className="card">
<h2>{animal.name}</h2>
<button onClick={() => animal.onClick.current(animal.index)}>Remove</button>
</div>
);
}
I hope, It will be useful for you.
Let's say I have a <SelectPicker/> component where I can select an option. What I want is how to add another <SelectPicker/> after I selected an option.
function DynamicComponent() {
const [state, setState] = useState([
{ name: null, id: '1' },
]);
const handleAdd = (value) => {
// Updating logic
};
return(
<>
{ state.map(item => {
return <SelectPicker
onSelect={handleAdd}
key={item.id}
value={item.name}
data={options} />
})
}
</>
);
}
In the example above, let's say there is default SelectPicker which is not selected. After selection, I think handleAdd function should update object that has id equal to '1' and add another object like this { name: null, id: '2' }.
What is the best way to achieve such functionality in react? Any help would be appreciated.
Thank you.
On an abstract level, what you want to do is have an array of components inside your state which is then called by the Render function of DynamicComponent. This array should get its first SelectPicker component immediately, and every time handleAdd is called you add a new SelectPicker to the array using your setState function. You can get the id for each new SelectPicker component by finding array.length.
In addition to the question, the below note from OP is also addressed in the below question
what if I want to update object's name property that has id:1 and add
new object to state at the same time?
This may be one possible solution to achieve the desired result:
function DynamicComponent() {
const [myState, setMyState] = useState([
{name: null, id: '1'}
]);
const handleAdd = arrIdx => setMyState(prev => {
const newArr = [...prev];
prev[arrIdx]?.name = ">>>>----new name goes here---<<<<";
return [
...newArr,
{
name: null,
id: (prev.length + 1).toString()
}
]
});
return(
<div>
{myState.map((item, idx) => (
<SelectPicker
onSelect={() => handleAdd(idx)}
key={item.id}
value={item.name}
data={options}
/>
)}
</div>
);
}
NOTES
Avoid using variable-names such as "state"
Passed the "index" from the .map() iteration
This helps in tracking the exact array-element
The new element with name: null is added as the last
The id is calculated by incrementing the current length by 1
I have a React Native Flatlist that only re-renders when its data has changed.
I give it the following data (as prop):
const posts = [
{
...post1Data
},
{
...post2Data
},
{
...post3Data
},
{
...post4Data
},
{
...post5Data
},
]
And here is my FlatList renderItem:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
How can I add an AdMob ad between the FlatList data with a probability of 5% without skiping any data in the posts array?
I have tried this:
const renderItem = useCallback(({ item, index }) => {
const { id, userData, images, dimensions, text } = item;
if (Math.random() < 0.05) return <Ad ... />
return (
<View
onLayout={(event) => {
itemHeights.current[index] = event.nativeEvent.layout.height;
}}
>
<Card
id={id}
cached={false}
userData={userData}
images={images}
dimensions={dimensions}
text={text}
/>
</View>
);
}, []);
But this causes 2 problems:
Some items from data are skipped (not returned)
When the flatlist re-renders (because of some of its props changes) the ads might disappear (there is a chance of 95%).
Any ideas? Should I render the ads randomly in the footer of my Card component like this?
const Card = memo ((props) => {
...
return (
<AuthorRow ... />
<Content ... />
<SocialRow ... /> {/* Interaction buttons */}
<AdRow />
)
}, (prevProps, nextProps) => { ... });
const AdRow = memo(() => {
return <Ad ... />
}, () => true);
I am not really sure about this option, it works but it could violate the admob regulations (?) (because I am adapting the ad to the layout of my card component)
I would appreciate any kind of guidance/help. Thank you.
I'm not sure if you ever found a solution to this problem, but I accomplished this by injecting "dummy" items into the data set, then wrapping the renderItem component with a component that switches based on the type of each item.
Assuming your flatlist is declared like this:
<FlatList data={getData()} renderItem={renderItem}/>
And your data set is loaded into a variable called sourceData that is tied to state. Let's assume one entry in your sourceData array looks like this. Note the 'type' field to act as a type discriminator:
{
"id": "d96dce3a-6034-47b8-aa45-52b8d2fdc32f",
"name": "Joe Smith",
"type": "person"
}
Then you could declare a function like this:
const getData = React.useCallback(() => {
let outData = [];
outData.push(...sourceData);
// Inject ads into array
for (let i = 4; i < outData.length; i += 5)
{
outData.splice(i, 0, {type:"ad"});
}
return outData;
}, [sourceData]);
... which will inject ads into the data array between every 4th item, beginning at the 5th item. (Since we're pushing new data into the array, i += 5 means an ad will be placed between every 4th item. And let i = 4 means our first ad will show after the 5th item in our list)
Finally, switch between item types when you render:
const renderItem = ({ item }) => (
item.type === 'ad'
?
<AdComponent ...props/>
:
<DataComponent ...props/>
);
I am pretty new to react. So I have one parent component which has two child components. These 2 children are the lists that should be displayed. So far I figured out how to transfer the data between two lists by checking the status property of the data. I am not able to understand how to add data into the separate lists and edit them since the parent component renders the 2 lists. Can anyone explain how to add and edit new data that the user will enter? Should I create new states and props on the Items page or should I create them on the child component page? I am pretty confused.
import React,{useState,useEffect} from 'react'
import { Completed } from './Completed'
import { Pending } from './Pending'
export const Items = () => {
const [items,setItems]=useState([
{
id: 1,
title:'Workout',
status:'Pending'
},
{
id: 2,
title:'Read Books',
status:'Pending'
},
{
id: 3,
title:'Cook Pizza',
status:'Pending'
},
{
id: 4,
title:'Pay Bills',
status:'Completed'
},
{
id: 5,
title:' Watch Big Short',
status:'Completed'
},
{
id: 6,
title:' Make nutrition Plan',
status:'Pending'
}
])
const updateStatus=(id,newStatus)=>{
let allItems=items;
allItems=allItems.map(item=>{
if(item.id===id){
console.log('in here')
item.status=newStatus;
}
return item
})
setItems(allItems)
}
return (
<div class="items">
<Pending items={items} setItems={setItems} updateStatus={updateStatus}/>
<Completed items={items} setItems={setItems} updateStatus={updateStatus}/>
</div>
)
}
import React from 'react'
export const Pending = ({items,setItems,updateStatus}) => {
return (
<div className="pending">
<h1>LEFT</h1>
{
items && items.map(item=>{
if(item && item.status==='Pending')
return <><p className="item" key={item.id}>{item.title} <button className="mark_complete" key={item.id} onClick={()=>{updateStatus(item.id,'Completed')}}>Move Right</button></p></>
})
}
</div>
)
}
import React from 'react'
export const Completed = ({items,setItems,updateStatus}) => {
return (
<div className="completed">
<h1>RIGHT</h1>
<form onSubmit={this.addItem}>
<input placeholder="enter task">
</input>
<button type="submit">add</button>
</form>
{
items && items.map(item=>{
if(item && item.status==='Completed')
return <><p className="item" key={item.id}>{item.title} <button className="mark_pending" key={item.id} onClick={()=>{updateStatus(item.id,'Pending')}}> Move Left</button></p> </>
})
}
</div>
)
}
I have attached the 3 components which are Items, Pending and Completed above.
It's almost always better to have the state in the parent and pass down props to the children. So you want to keep your items state where it is. You can create an addItem function and pass it down as a prop to any child.
I don't think it makes sense to be able to add items from both lists since new items should be 'Pending'. So I would recommend that you put your add form in a new component AddItem which would be a third child of Items. Once AddItem calls the addItem function from props, that item will get saved to the state in items and it will show up in the Pending list automatically.
If all new items have status 'Pending' then the only information that we should need to add an item is the title of the task.
This function goes in Items:
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
Your AddItem component uses a controlled input to create the text for the new item.
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
Inside the return of Items, include your form:
<AddItem addItem={addItem} />
Unrelated to the question at hand, there are a few other improvements that you can make to your code.
Your updateStatus function actually mutates the current item. You should instead create a new object for the changed item by copying everything except the status.
You are getting warnings about unique keys because the key must be on the outermost component inside the .map(). You put a fragment <> outside the <p> which has the key, so remove the fragment.
In my opinion the filtering of which item goes in each list should be done by the parent. Your Completed and Pending components are extremely similar. You should combine them into one component. Everything that is different between the two, such as texts and class names, can be controlled by the props that you pass in.
import React, { useState } from "react";
export const ItemsList = ({
items,
title,
className,
buttonText,
onClickButton
}) => {
return (
<div className={className}>
<h1>{title}</h1>
{items.map((item) => (
<p className="item" key={item.id}>
<span className="item_title">{item.title}</span>
<button
className="move_item"
key={item.id}
onClick={() => {
onClickButton(item.id);
}}
>
{buttonText}
</button>
</p>
))}
</div>
);
};
// example of how to compose components
// this keeps the same setup that you had before, but without repeated code
export const Completed = ({ items, updateStatus }) => {
return (
<ItemsList
title="RIGHT"
buttonText="Move Left"
className="completed"
items={items.filter((item) => item.status === "Completed")}
onClickButton={(id) => updateStatus(id, "Pending")}
/>
);
};
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
export const Items = () => {
const [items, setItems] = useState([
{
id: 1,
title: "Workout",
status: "Pending"
},
{
id: 2,
title: "Read Books",
status: "Pending"
},
{
id: 3,
title: "Cook Pizza",
status: "Pending"
},
{
id: 4,
title: "Pay Bills",
status: "Completed"
},
{
id: 5,
title: " Watch Big Short",
status: "Completed"
},
{
id: 6,
title: " Make nutrition Plan",
status: "Pending"
}
]);
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
const updateStatus = (id, newStatus) => {
setItems((current) =>
// arrow function without braces is an implicit return
current.map((item) =>
item.id === id
? // copy to new item if id matches
{
...item,
status: newStatus
}
: // otherwise return the existing item
item
)
);
};
return (
<div className="items">
<AddItem addItem={addItem} />
{/* can set the props on ItemsList here */}
<ItemsList
title="LEFT"
buttonText="Move Right"
className="pending"
items={items.filter((item) => item.status === "Pending")}
// create a function that just takes the `id` and sets the status to "Completed"
onClickButton={(id) => updateStatus(id, "Completed")}
/>
{/* or do it in a separate component */}
<Completed items={items} updateStatus={updateStatus} />
</div>
);
};
export default Items;
Code Sandbox Link
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,
},
]);