I am experiencing this error with react dnd. The weird thing is that it depends on the key i specify to my react component. if i specify index, one part of my function fires this error, and when i specify item.id, another part doesnt fire. it doesnt make sense. please help.
When I specify the key to be index, the error fires when forum has no parent. however when i specify the key to be forum._id, the error fires when forum has parent. i dont know what to do, please help :)
Please visit this sandbox to reproduce:
https://codesandbox.io/s/proud-wind-hklt6
To reproduce:
Drag item 1ba on top of item 1, and then drag the item 1ba down the path.
Forum.jsx
const Forum = ({ forum, forums, setForums, move, find }) => {
const [{ isOver, canDrop }, drop] = useDrop({
accept: "forum",
hover: throttle((item, monitor) => {
if (item._id === forum._id) {
return;
}
if (!monitor.isOver({ shallow: true })) {
return;
}
if (!canDrop) return;
move(item, forum, forum.parent);
item = forum;
}, 200),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const [, drag, preview] = useDrag({
item: {
_id: forum._id,
title: forum.title,
type: "forum",
children: forum.children,
parent: forum.parent,
},
isDragging(props, monitor) {
return props._id == monitor.getItem()._id;
},
});
const getChildren = async (forumId) => {
const _forums = await ForumService.getChildren(forumId, forums);
setForums(_forums);
};
return (
<Wrapper ref={drop}>
<ForumContainer ref={drag}>
{!!forum.childrenIds?.length && (
<div>
{!forum.isOpen ? (
<ForumChevron
className="fas fa-chevron-down"
onClick={() => getChildren(forum._id)}
></ForumChevron>
) : (
<ForumChevron
className="fas fa-chevron-up"
onClick={() =>
setForums(ForumService.resetChildren(forum._id, forums))
}
></ForumChevron>
)}
</div>
)}
<ForumTitle>{forum.title}</ForumTitle>
</ForumContainer>
{forum.children && !!forum.children.length && (
<ForumChildrenWrapper>
{forum.children.map((child, index) => (
<Forum
forum={child}
setForums={setForums}
forums={forums}
key={index}
move={move}
find={find}
/>
))}
</ForumChildrenWrapper>
)}
</Wrapper>
);
};
export default Forum;
ForumManager.jsx
if (!item.parent) {
console.log("here 1");
const dest = findItem(afterItem._id, _forums);
if (!dest.children) dest.children = [];
foundItem.parent = afterItem._id;
const idx = _forums.findIndex((f) => f._id === item._id);
_forums.splice(idx, 1);
if (dest.parent === foundItem._id) {
dest.parent = "";
if (foundItem.children.length) {
// When key is item.id, error shows up here
console.log("parent & has children");
for (let child of [...foundItem.children]) {
if (child._id === dest._id) {
child.children.splice(0, 0, {
...foundItem,
children: [],
childrenIds: [],
});
}
_forums.push(child);
}
} else {
console.log("no children");
dest.children.unshift({
...foundItem,
children: [],
childrenIds: [],
});
}
} else {
// When key is index, error shows up here
console.log("no parent");
console.log(dest);
dest.parent = "";
dest.children.splice(0, 0, {
...foundItem,
children: [],
childrenIds: [],
});
}
}
Try adding debounce to the hover handler (with trailing option). The components are updating too quickly by setting the state before DnD could catch up, and the target ID had changed by the time the user dropped the item.
Also - don't use index as the key, as it will change each time.
If you remove monitor.canDrop() inside collect function, then it works. Not sure, but this is one way.
Related
I want to display by default only data where the status are Pending and Not started. For now, all data are displayed in my Table with
these status: Good,Pending, Not started (see the picture).
But I also want to have the possibility to see the Good status either by creating next to the Apply button a toggle switch : Show good menus, ( I've made a function Toggle.jsx), which will offer the possibility to see all status included Good.
I really don't know how to do that, here what I have now :
export default function MenuDisplay() {
const { menuId } = useParams();
const [selected, setSelected] = useState({});
const [hidden, setHidden] = useState({});
const [menus, setMenus] = useState([]);
useEffect(() => {
axios.post(url,{menuId:parseInt(menuId)})
.then(res => {
console.log(res)
setMenus(res.data.menus)
})
.catch(err => {
console.log(err)
})
}, [menuId]);
// If any row is selected, the button should be in the Apply state
// else it should be in the Cancel state
const buttonMode = Object.values(selected).some((isSelected) => isSelected)
? "apply"
: "cancel";
const rowSelectHandler = (id) => (checked) => {
setSelected((selected) => ({
...selected,
[id]: checked
}));
};
const handleClick = () => {
if (buttonMode === "apply") {
// Hide currently selected items
const currentlySelected = {};
Object.entries(selected).forEach(([id, isSelected]) => {
if (isSelected) {
currentlySelected[id] = isSelected;
}
});
setHidden({ ...hidden, ...currentlySelected });
// Clear all selection
const newSelected = {};
Object.keys(selected).forEach((id) => {
newSelected[id] = false;
});
setSelected(newSelected);
} else {
// Select all currently hidden items
const currentlyHidden = {};
Object.entries(hidden).forEach(([id, isHidden]) => {
if (isHidden) {
currentlyHidden[id] = isHidden;
}
});
setSelected({ ...selected, ...currentlyHidden });
// Clear all hidden items
const newHidden = {};
Object.keys(hidden).forEach((id) => {
newHidden[id] = false;
});
setHidden(newHidden);
}
};
const matchData = (
menus.filter(({ _id }) => {
return !hidden[_id];
});
const getRowProps = (row) => {
return {
style: {
backgroundColor: selected[row.values.id] ? "lightgrey" : "white"
}
};
};
const data = [
{
Header: "id",
accessor: (row) => row._id
},
{
Header: "Name",
accessor: (row) => (
<Link to={{ pathname: `/menu/${menuId}/${row._id}` }}>{row.name}</Link>
)
},
{
Header: "Description",
//check current row is in hidden rows or not
accessor: (row) => row.description
},
{
Header: "Status",
accessor: (row) => row.status
},
{
Header: "Dishes",
//check current row is in hidden rows or not
accessor: (row) => row.dishes,
id: "dishes",
Cell: ({ value }) => value && Object.values(value[0]).join(", ")
},
{
Header: "Show",
accessor: (row) => (
<Toggle
value={selected[row._id]}
onChange={rowSelectHandler(row._id)}
/>
)
}
];
const initialState = {
sortBy: [
{ desc: false, id: "id" },
{ desc: false, id: "description" }
],
hiddenColumns: ["dishes", "id"]
};
return (
<div>
<button type="button" onClick={handleClick}>
{buttonMode === "cancel" ? "Cancel" : "Apply"}
</button>
<Table
data={matchData}
columns={data}
initialState={initialState}
withCellBorder
withRowBorder
withSorting
withPagination
rowProps={getRowProps}
/>
</div>
);
}
Here my json from my api for menuId:1:
[
{
"menuId": 1,
"_id": "123ml66",
"name": "Pea Soup",
"description": "Creamy pea soup topped with melted cheese and sourdough croutons.",
"dishes": [
{
"meat": "N/A",
"vegetables": "pea"
}
],
"taste": "Good",
"comments": "3/4",
"price": "Low",
"availability": 0,
"trust": 1,
"status": "Pending",
"apply": 1
},
//...other data
]
Here my CodeSandbox
Here a picture to get the idea:
Here's the second solution I proposed in the comment:
// Setting up toggle button state
const [showGood, setShowGood] = useState(false);
const [menus, setMenus] = useState([]);
// Simulate fetch data from API
useEffect(() => {
async function fetchData() {
// After fetching data with axios or fetch api
// We process the data
const goodMenus = dataFromAPI.filter((i) => i.taste === "Good");
const restOfMenus = dataFromAPI.filter((i) => i.taste !== "Good");
// Combine two arrays into one using spread operator
// Put the good ones to the front of the array
setMenus([...goodMenus, ...restOfMenus]);
}
fetchData();
}, []);
return (
<div>
// Create a checkbox (you can change it to a toggle button)
<input type="checkbox" onChange={() => setShowGood(!showGood)} />
// Conditionally pass in menu data based on the value of toggle button "showGood"
<Table
data={showGood ? menus : menus.filter((i) => i.taste !== "Good")}
/>
</div>
);
On ternary operator and filter function:
showGood ? menus : menus.filter((i) => i.taste !== "Good")
If button is checked, then showGood's value is true, and all data is passed down to the table, but the good ones will be displayed first, since we have processed it right after the data is fetched, otherwise, the menus that doesn't have good status is shown to the UI.
See sandbox for the simple demo.
I have following part of React code:
This is handler for adding new object item to the array.
So I am checking if object exist in whole array of objects. If yes I want to increase quantity of this object by 1. Else I want to add object and set quantity to 1.
I am getting error when I want to add 2nd same product (same id)
Uncaught TypeError: Cannot assign to read only property 'quantity' of object '#<Object>'
export const Component = ({ book }: { book: Book }) => {
const [basket, setBasket] = useRecoilState(Basket);
const handleAddBookToBasket = () => {
const find = basket.findIndex((product: any) => product.id === book.id)
if (find !== -1) {
setBasket((basket: any) => [...basket, basket[find].quantity = basket[find].quantity + 1])
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }])
}
}
EDIT:
if (find !== -1) {
setBasket((basket: any) =>
basket.map((product: any) => ({
...product,
quantity: product.quantity + 1,
}))
);
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }]);
}
data structures
I'd say the root of your "problem" is that you chose the wrong data structure for your basket. Using Array<Item> means each time you need to update a specific item, you have to perform O(n) linear search.
If you change the basket to { [ItemId]: Item } you can perform instant O(1) updates. See this complete example below. Click on some products to add them to your basket. Check the updated basket quantity in the output.
function App({ products = [] }) {
const [basket, setBasket] = React.useState({})
function addToBasket(product) {
return event => setBasket({
...basket,
[product.id]: {
...product,
quantity: basket[product.id] == null
? 1
: basket[product.id].quantity + 1
}
})
}
return <div>
{products.map((p, key) =>
<button key={key} onClick={addToBasket(p)} children={p.name} />
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
const products = [
{ id: 1, name: "ginger" },
{ id: 2, name: "garlic" },
{ id: 3, name: "turmeric" }
]
ReactDOM.render(<App products={products}/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
object update
As a good practice, you can make a function for immutable object updates.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
export { update }
Then import it where this behavior is needed. Notice it's possible to use on nested object updates as well.
// App.js
import * as Obj from "./Obj"
function App({ products = [] }) {
// ...
function addToBasket(product) {
return event => setBasket(
Obj.update(basket, product.id, (p = product) => // ✅
Obj.update(p, "quantity", (n = 0) => n + 1) // ✅
)
)
}
// ...
}
object remove
You can use a similar technique to remove an item from the basket. Instead of coupling the behavior directly in the component that needs removal, add it to the Obj module.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
const remove: (o, key) => { // ✅
const r = { ...o }
delete r[key]
return r
}
export { update, remove } // ✅
Now you can import the remove behaviour in any component that needs it.
function App() {
const [basket, setBasket] = React.useState({
1: { id: 1, name: "ginger", quantity: 5 },
2: { id: 2, name: "garlic", quantity: 6 },
3: { id: 3, name: "tumeric", quantity: 7 },
})
function removeFromBasket(product) {
return event => {
setBasket(
Obj.remove(basket, product.id) // ✅
)
}
}
return <div>
{Object.values(basket).map((p, key) =>
<div key={key}>
{p.name} ({p.quantity})
<button onClick={removeFromBasket(p)} children="❌" />
</div>
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
// inline module for demo
const Obj = {
remove: (o, key) => {
const r = {...o}
delete r[key]
return r
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
In your setBasket your should create a new object instead of updating current one
your code should look like these one :
{...basket, ...{
quantity : basket.quantity + 1
}}
I'm doing a facebook clone, and everytime i press like's button, i want to see the change immediately, that's something that swr provides, but, it only updates after 4-8 seconds :/
What i tried to do is the following: when i click like's button, i first mutate the cache that swr provides, then i make the call to the API, then revalidate data to see if everything is right with the data, actually i console log the cache and it updates immediately, but it the UI doesn't and i don't know why
Let me give sou some context with my code
This is how my publication looks like ( inside pub it's the likes property )
export type theLikes = {
identifier: string;
};
export type theComment = {
_id?: string;
body: string;
name: string;
perfil?: string;
identifier: string;
createdAt: string;
likesComments?: theLikes[];
};
export interface Ipublication {
_id?: string;
body: string;
photo: string;
creator: {
name: string;
perfil?: string;
identifier: string;
};
likes?: theLikes[];
comments?: theComment[];
createdAt: string;
}
export type thePublication = {
data: Ipublication[];
};
This is where i'm asking for all publications with getStaticProps
const PublicationsHome = ({ data: allPubs }) => {
// All pubs
const { data: Publications }: thePublication = useSWR(
`${process.env.URL}/api/publication`,
{
initialData: allPubs,
revalidateOnFocus: false
}
);
return (
<>
{Publications ? (
<>
<PublicationsHomeHero>
{/* Create pub */}
<CreatePubs />
{/* Show pub */}
{Publications.map(publication => {
return <Pubs key={publication._id} publication={publication} />;
})}
</PublicationsHomeHero>
</>
) : (
<div></div>
)}
</>
);
};
export const getStaticProps: GetStaticProps = async () => {
const { data } = await axios.get(`${process.env.URL}/api/publication`);
return {
props: data
};
};
export default PublicationsHome;
This is where the like button is ( focus on LikePub, there is where the logic is )
The conditional is simple, if user already liked a pub, cut the like, otherwise, like the pub
interface IlikesCommentsProps {
publication: Ipublication;
}
const LikesComments: React.FC<IlikesCommentsProps> = ({ publication }) => {
const LikePub = async (): Promise<void> => {
try {
if (publication.likes.find(f => f.identifier === userAuth.user.id)) {
mutate(
`${process.env.URL}/api/publication`,
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const deleteLike = currentPub.likes.findIndex(
f => f.identifier === userAuth.user.id
);
currentPub.likes.splice(deleteLike, 1);
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
},
false
);
} else {
mutate(
`${process.env.URL}/api/publication`,
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
currentPub.likes.push({ identifier: userAuth.user.id });
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
},
false
);
}
console.log(publication.likes);
await like({ identifier: userAuth.user.id }, publication._id);
mutate(`${process.env.URL}/api/publication`);
} catch (err) {
if (err) {
mutate(`${process.env.URL}/api/publication`);
}
}
};
return (
<motion.div
onClick={LikePub}
variants={Actions}
whileHover="whileHover"
whileTap="whileTap"
>
<motion.span>
<LikeIcon colorIcon="rgb(32, 120, 244)" />
</motion.span>
<LikeCommentText
separation="0.2rem"
colorText="rgb(32, 120, 244)"
>
Me gusta
</LikeCommentText>
</motion.div>
)
}
As you can see as well, i console.log publication's likes, and this is what happened
The identifier is added to the cache, meaning that user liked the pub, but the UI doesn't update, it takes 4 - 7 seconds to update, probably even more, the same thing happened with removing the like, look at this
Cut the like, but, the UI doesn't update
I'm desperate, i've tried everything, i've been trying to fix this for almost a week, but found nothing, what am i doing wrong, is this a bug ?
I believe the problem is you're directly mutating (in the javascript not swr sense) swr's data that is completely invisible to swr. And only when response is returned from the API your state is updated and that finally triggers swr's observers.
Here you may notice that currentPub.likes is an array (reference) inside currentPub object. You're directly mutating it (with splice) and then insert the same reference back into allPublications object. From swr's perspective the likes array didn't change. It still holds the same reference as before the mutation:
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const deleteLike = currentPub.likes.findIndex(
f => f.identifier === userAuth.user.id
);
currentPub.likes.splice(deleteLike, 1);
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
}
Snippet to illustrate the behavior:
const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
currentPub.likes.splice(1, 1)
const updatePub = allPublications.map((pub, idx) => idx === 0 ? currentPub : pub)
console.log(updatePub[0] === allPublications[0]) // true
console.log(updatePub[0].likes === allPublications[0].likes) // true. the reference stays the same
console.log(updatePub[0]) // while the object has changed
You should rewrite it to exlude direct mutation and always return changed references for changed objects/arrays. Something like:
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const likes = currentPub.likes.filter( // filter creates new array (reference)
f => f.identifier !== userAuth.user.id
);
const updatePub = allPublications.map(pub => // map creates new array
pub._id === currentPub._id ? { ...currentPub, likes } : pub // {} creates new object (reference)
);
return updatePub;
}
const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
const likes = currentPub.likes.filter((el) => el !== 2)
const updatePub = allPublications.map((pub, idx) =>
idx === 0 ? { ...currentPub, likes } : pub
)
console.log(updatePub[0] === allPublications[0]) // false
console.log(updatePub[0].likes === allPublications[0].likes) // false
console.log(updatePub[0])
And do the same for else branch mutation function.
I am trying to implement an "add all" button in my react app. to do that, i pass this function to the onClick method of the button :
for (element in elements) {
await uploadfunction(element)
}
const uploadfunction = async (element) => {
if (valid) {
// await performUpload(element)
}
else if (duplicate) {
//show dialog to confirm upload - if confirmed await performUpload(element)
}
else {
// element not valid set state and show failed notification
}
}
const performUpload = async (element) => {
// actual upload
if(successful){
// set state
}else{
// element not successful set state and show failed notification
}
}
the uploadfunction can have three different behaviors :
Add the element to the database and update the state
Fail to add the element and update the state
Prompt the user with the React Dialog component to ask for confirmation to add duplicat element and update the state accordingly
My problem now is since i'm using a for loop and despite using Async/await , i can't seem to wait for user interaction in case of the confirmation.
The behavior i currently have :
The for loop move to the next element no matter what the result
The Dialog will show only for a second and disappear and doesn't wait for user interaction
Wanted behavior:
Wait for user interaction (discard/confirm) the Dialog to perform the next action in the loop.
How can i achieve that with React without Redux ?
Here is an example of a component that might work as an inspiration for you.
You might split it in different components.
class MyComponent extends Component {
state = {
items: [{
// set default values for all booleans. They will be updated when the upload button is clicked
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_1',
}, {
isValid: true,
isDuplicate: false,
shouldUploadDuplicate: false,
data: 'element_2',
}],
performUpload: false,
};
onUploadButtonClick = () => {
this.setState(prevState => ({
...prevState,
items: prevState.items.map((item, index) => ({
isValid: validationFunction(),
isDuplicate: prevState.items.slice(0, index).some(i => i.data === item.data),
shouldUploadDuplicate: false,
data: item.data
})),
performUpload: true,
}), (nextState) => {
this.uploadToApi(nextState.items);
});
};
getPromptElement = () => {
const firstDuplicateItemToPrompt = this.getFirstDuplicateItemToPrompt();
const firstDuplicateItemIndexToPrompt = this.getFirstDuplicateItemIndexToPrompt();
return firstDuplicateItemToPrompt ? (
<MyPrompt
item={item}
index={firstDuplicateItemIndexToPrompt}
onAnswerSelect={this.onPromptAnswered}
/>
) : null;
};
getFirstDuplicateItemToPrompt = this.state.performUpload
&& !!this.state.items
.find(i => i.isDuplicate && !i.shouldUploadDuplicate);
getFirstDuplicateItemIndexToPrompt = this.state.performUpload
&& !!this.state.items
.findIndex(i => i.isDuplicate && !i.shouldUploadDuplicate);
onPromptAnswered = (accepted, item, index) => {
this.setState(prevState => ({
...prevState,
items: prevState.items
.map((i, key) => (index === key ? ({
...item,
shouldUploadDuplicate: accepted,
}) : item)),
performUpload: accepted, // if at last an item was rejected, then the upload won't be executed
}));
};
uploadToApi = (items) => {
if (!this.getFirstDuplicateItemToPrompt()) {
const itemsToUpload = items.filter(i => i.isValid);
uploadDataToApi(itemsToUpload);
}
};
render() {
const { items } = this.stat;
const itemElements = items.map((item, key) => (
<MyItem key={key} {...item} />
));
const promptElement = this.getPromptElement();
return (
<div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{itemElements}
</div>
<Button onClick={this.onUploadButtonClick}>Upload</Button>
{promptElement}
</div>
)
}
}
I'm getting data from a payload which has a total number of likes on each post. On the user screen, there's an icon for the user to like a post and what i want to achieve is when the user taps on it, the value show be increased to plus 1 against that particular post
VIEW:
{
posts.map((item, i) => {
return (
<View key={i} style={styles.user}>
<Card>
<ListItem
titleStyle={{ color: '#36c', fontWeight:'500' }}
titleNumberOfLines={2}
hideChevron={false}
roundAvatar
title={item.headline}
avatar={{uri:'https://s3.amazonaws.com/uifaces/faces/twitter/brynn/128.jpg'}}
/>
<Text style={{marginBottom: 10, fontSize:16, color:'#4a4a4a', fontFamily:'HelveticaNeue-Light'}}>
{item.text}
</Text>
<TouchableOpacity style={styles.likeContainer}>
<Text style={{fontSize:14}}>{item.likesCount}{"\n"}</Text>
<Icon
onPress={()=>onLikePost(item)}
name='md-thumbs-up'
type='ionicon'
iconStyle={[(item.isLiked=== true) ? styles.likedColor : styles.unLikedColor]}
/>
</TouchableOpacity>
</Card>
</View>
);
})
}
CONTAINER:
state = {
posts : [],
id: '',
user: ''
}
componentDidMount = () => {
const { navigation } = this.props;
this.setState({
id : navigation.getParam('id'),
user: navigation.getParam('user')
}, ()=> this.getData())
}
getData = () => {
const api = create({
baseURL: 'https://url.com/api',
headers: {'Accept': 'application/json'}
});
api.get('/groups/'+`${this.state.groupID}`+'/posts').then((response) => {
let data = response.data.data
this.setState({ posts: data });
console.log(JSON.stringify(this.state.posts))
})
}
onLikePost = (item) => {
item.likeCount = item.likeCount+1
}
You are storing posts data in state variable so use setState to update it. Use map and check for each post, whenever id (unique property of each post) matches to id of the clicked item, increase its likesCount otherwise return the same data.
Write it like this:
onLikePost = (item) => {
this.setState(prevState => ({
posts: prevState.posts.map(el => el.id === item.id? {...el, likesCount: el.likesCount+1} : el)
}))
}
Update: Put the check before updating the count value and change the isLiked bool also.
onLikePost = (item) => {
this.setState(prevState => ({
posts: prevState.posts.map(el => {
if(el.id === item.id) {
return {
...el,
isLiked: !el.isLiked,
likesCount: !el.isLiked? el.likesCount+1 : el.likesCount-1,
}
}
return el;
})
}))
}
Note: I am assuming each post has a key id unique value, if it doesn't exist then use any other unique property of the each post.
If array sequence is not an issue, you can use item index and use setState to update it.
<Icon
onPress={()=>onLikePost(i)}
...
/>
...
onLikePost = (i) => {
let posts = this.state.posts;
posts[i].likesCount = !posts[i].isLiked ? posts[i].likesCount + 1 : posts[i].likesCount - 1;
posts[i].isLiked = !posts[i].isLiked;
this.setState({ posts: posts})
}