Deleting Objects in State While Rendering State in Reverse - javascript

Making a comment section for a website and I ran into a big problem. Currently I have a delete button that splices the comments from state based on their index. I need to show the array in reverse to the user--so when they make multiple comments the newest one is ontop.
The problem is if I reverse() the mapped array the index doesn't get reversed with it, so clicking delete for item 1 deletes the last item, and vice versa.
const [userComments, setUserComments] = useState([])
const postComment = (event, userComment) => {
if (event.key === 'Enter') {
setUserComments(prevState => ([...prevState, {comment: userComment}]))
}
}
const deleteComment = (e, i) => {
const userCommentsArray = [...userComments]
userCommentsArray.splice(i, 1)
setUserComments(prevState => ([...prevState], userCommentsArray))
}
return (
<input
placeholder="Add a public comment"
onKeyUp={event => postComment(event, event.currentTarget.value)}
onClick={event => showCommentButtons()}
/>
{ userComments
? userComments.map((item, i) => (
<div className="button" onClick={e => deleteComment(e, i)}>Button</div>
<p className="comment">{item.comment}</p>
))
: null
}
)

Use reverse method on array:
const deleteComment = (e, i) => {
const userCommentsArray = [...userComments].reverse()
userCommentsArray.splice(i, 1)
setUserComments(prevState => ([...prevState], userCommentsArray.reverse()))
}

Figured it out. Used unshift to push the items to state in reverse order.
No other changes necessary.
const postComment = (userComment) => {
const userCommentNotBlank = !userComment.trim().length < 1
if (userCommentNotBlank) {
const newState = [...userComments]
newState.unshift(userComment)
setUserComments(prevState => ([...prevState], newState))
resetAddComment()
}
}

Related

getting an intial copy of redux state on render to reset it after a button click

I've got some buttons in my navbar when clicked return a filtered state. These objects are then used to render cards in my UI.
<div className="filter-buttons">
<button className="filter-btn" onClick={() => setFilter("nature")}>Nature</button>
<button className="filter-btn" onClick={() => setFilter("tech")}>Tech</button>
<button className="filter-btn" onClick={() => setFilter("education")}>Education</button>
</div>
I'm using redux toolkit library for my state. I pull in all blog objects and store it into a variable. I have a useEffect hook that runs everytime a button is clicked to try and reset the state and filter all the objects again.
const blogsCopy = JSON.stringify(useSelector((state) => state.blog));
const allBlogs = useSelector((state) => state.blog);
const myBlogs = useSelector((state) => state.user.blogs);
const [filter, setFilter] = useState("");
const originalState = useMemo(() => JSON.parse(blogsCopy), []);
useEffect(() => {
setNavFilterLoading(true);
let filteredBlogs = [];
const keys = Object.keys(allBlogs);
if (filter.startsWith("nat")) {
// reset the state to the original state
dispatch(setWholeState(originalState));
console.log("original state", originalState);
filteredBlogs = keys.filter(key => allBlogs[key].tags.some(tag => tag.startsWith("nat"))).map((key, index) => ({index, ...allBlogs[key]})
);
} else if (filter.startsWith("tech")) {
filteredBlogs = keys.filter(key => allBlogs[key].tags.some(tag => tag.startsWith("tech"))).map((key, index) => ({index, ...allBlogs[key]})
);
} else if (filter.startsWith("edu")) {
filteredBlogs = keys.filter(key => allBlogs[key].tags.some(tag => tag.startsWith("edu"))).map((key, index) => ({index, ...allBlogs[key]})
);
} else {
filteredBlogs = keys.map((key, index) => ({index, ...allBlogs[key]})
);
}
dispatch(setWholeState(filteredBlogs));
setNavFilterLoading(false);
}, [filter]);
Issue with this implementation is, the object gets mutated and I'm not able to reset the state again. If I clicked on the nature button, it will return 3 objects. If one of the three includes a 'tech' tag. It will filter the three objects and return one object.
I want it to reset the whole state to an initial copy to properly filter it out. Not filter what has been filtered.
I think the use of useEffect to save filteredBlogs to the store might not be necessary here since filter seems to be for local use only.
Assuming that the goal is to filter data for conditional display, and the data retrieved from store works as expected, perhaps there is no need to mutate the data, instead a local filteredBlogs can be created within the component.
A rough example could be something like:
const blogsCopy = JSON.stringify(useSelector((state) => state.blog));
const allBlogs = useSelector((state) => state.blog);
const myBlogs = useSelector((state) => state.user.blogs);
const [filter, setFilter] = useState("");
const filteredBlogs = allBlogs.filter((item) =>
filter ? item.tags.some((tag) => tag.startsWith(filter)) : true
);
Local filteredBlogs can then be used for output with filteredBlogs.map() and it would be updated when the filter state change.
Perhaps the buttons could also be changed to save the shorter value as filter, for simpler logic:
<div className="filter-buttons">
<button className="filter-btn" onClick={() => setFilter("")}>Clear all</button>
<button className="filter-btn" onClick={() => setFilter("nat")}>Nature</button>
<button className="filter-btn" onClick={() => setFilter("tech")}>Tech</button>
<button className="filter-btn" onClick={() => setFilter("edu")}>Education</button>
</div>

How to only add unique values to a useState array and maintain maximum of length 3

So, I am trying to add max 3 values to a useState array and ensure all are unique and if fourth unique element is pushed it will replace the first element at index 0. ? Also, if the same element is clicked then it is removed from the array. However the function I wrote is not able to accomplish my objective.
const [chatbox,setChatBox]=useState([]);
chat=['a11','b11','c11','d11']
chat.map((c)=>(
<div
onClick={()=>addorRemoveitems(c)}
>
{c}
<div>
))
const addorRemoveitems=(cId)=>{
if (
chatbox.length <= 3 &&
chatbox.includes(cId)
) {
const newArray = chatbox.filter(
(c) => c !== cId
);
setChatBox(newArray);
} else if (
chatbox.length < 3 &&
chatbox.indexOf(cId) === -1
) {
setChatBox((prev) => [...prev, cId]);
} else if (
chatbox.length == 3 &&
chatbox.indexOf(cId) !== -1
) {
const newArray = chatbox.splice(
0,
1,
cId
);
setChatBox(newArray);
}
}
You'll have to modify this a tad I guess to fit how you are adding new items to the list. Here is a working example: https://codesandbox.io/s/react-hooks-playground-forked-e32wlp?file=/src/index.tsx
const [array, setArray] = useState([]);
const [newItemGen, setNewItemGen] = useState(0);
const addOrRemoveItemFromArray = (item) => {
if (array.includes(item)) {
setArray((prevState) =>
prevState.filter((existing) => existing !== item)
);
} else {
setArray((prevState) => [item, ...prevState.slice(0, 2)]);
setNewItemGen((prevState) => prevState + 1);
}
};
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<div>
Items in array :
{array.map((item) => (
<button onClick={() => addOrRemoveItemFromArray(item)}>{item}</button>
))}
</div>
<div>
<button
onClick={() => addOrRemoveItemFromArray(`newItem${newItemGen}`)}
>
add item
</button>
</div>
</div>
);
}
If the item is already in the array, filter it out. If not, add the item to a slice of the old array which only captures the first two values.
EDIT:
I just realised you want to check that a value is unique before adding it to the list. You'll have to create a function for adding and function for removing then.
const addItem = (item) => {
if (!array.includes(item)) {
setArray((prevState) => [item, ...prevState.slice(0, 2)]);
setNewItemGen((prevState) => prevState + 1);
}
};
const removeItemFromArray = (item) => {
setArray((prevState) =>
prevState.filter((existing) => existing !== item)
);
};

Not rendering while state is changed

I'm learning react. I am trying to sort a list based on name. The ShoppingList component is
const ShoppingList = () => {
const [items, setItems] = useState([]);
const data = [
{id: 1, name: 'Soda'},
{id: 2, name: 'ice'},
];
useEffect(() => {
setItems(data);
}, []);
const handleSort = () => {}
return ();
}
On a button click I'm trying to sort the data and display it.
<button onClick={() => handleSort()}>Sort by name</button>
Inside the handleSort() function
const sortItems = items.sort((a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if(nameA < nameB)
return -1;
if(nameA > nameB)
return 1;
return 0;
});
console.log(sortItems);
setItems(sortItems);
The console.log(sortItems) shows the sorted array. But not rendering in the DOM.
Inside the return, I'm trying to display the sorted data in this format
<ul>
{items.map((item) => {
return (
<li key={item.id}>
<span>{item.name} </span>
<button onClick={() => handleRemove(item.id)}>×</button>
</li>
);
})
}
</ul>
What i'm missing here?
I'd recommend deriving the sorted list of items with useMemo, so it's "derived state" dependent on the items array and the desired sort order.
Don't use useEffect for initial state. useState accepts a creator function for the initial state instead.
localeCompare is a neater way to return -1, 0, +1 for comparison.
[...items] (a shallow copy of items) is required, because .sort() sorts an array in-place.
const sortByName = (a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase());
const ShoppingList = () => {
const [items, setItems] = useState(() => [
{ id: 1, name: "Soda" },
{ id: 2, name: "ice" },
]);
const [sortOrder, setSortOrder] = useState("original");
const sortedItems = React.useMemo(() => {
switch (sortOrder) {
case "byName":
return [...items].sort(sortByName);
default:
return items;
}
}, [items, sortOrder]);
return (
<>
<button onClick={() => setSortOrder("byName")}>Sort by name</button>
<button onClick={() => setSortOrder("original")}>Sort in original order</button>
<ul>
{sortedItems.map((el, i) => (
<li key={el.id}>
<span>{el.name} </span>
<button>×</button>
</li>
))}
</ul>
</>
);
};
First of all you need to stop using useEffect for the initial state,
And if you want react to notice your changes, use an object instead of array. (This is not always that react doesn't notice your changes, but since you didn't change array and it was only sorted, react ignores it).
const ShoppingList = () => {
const [items, setItems] = useState({
data: [
{ id: 1, name: 'Soda' },
{ id: 2, name: 'ice' },
],
});
const handleSort = () => {
const sortedItems = items.data.sort((a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
});
setItems({
data: sortedItems,
});
};
return (
<>
<button onClick={() => handleSort()}>Sort by name</button>
<ul>
{items.data.map((el, i) => (
<li key={el.id}>
<span>{el.name} </span>
<button onClick={() => handleRemove(item.id)}>×</button>
</li>
))}
</ul>
</>
);
}
Hope this helps 🙂
If you are interested to know more indepth on why the array items is changed (sorted) but React doesn't render, there are 2 things to take note:
How array.sort work
How React re-render with useState
For (1), it's easy, array.sort return the sorted array. Note that the array is sorted in place, and no copy is made. Hence sortItems and items still refer to the same array
For (2), it's a little bit complicated as we have to read through React code base.
This is React.useState signature
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
Use Github navigation tools and scan we gonna end-up to this code:
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
Next we must find the definition of mounState:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
...
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
Notice, the return type of mountState is an array where the 2nd argument is an function just like const [items, setItems] = useState([])
Which means we almost there.
dispatch is the value from dispatchAction.bind
Scan through the code we gonna end up at this line:
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
Last part is what function is does:
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
It simply check for equality using === operator.
Comeback to your sort function, nextState in our case is sortItems and prevState is items. With (1) in mind, sortItems === items => true so React gonna skip the rendering.
That's why you see most of the tutorials states that you have to do shallow copy.
By doing so your nextState will differ from your prevState.
TLDR:
React use function is above to check for state changes when using hooks
Always make shallow copy when working with array, object if you are using hooks

Problem with indexOf in an array of objects. React app

I'm having a following situation where I want to know the indexOf value to be able to use that knowledge later on in the code. How ever, I've tried multiple different ways to get the value and I don't seem to get it right. In the code below I've tried a few different ways that I found searching Stackoverflow. All of them return -1 so far, meaning that there is either something wrong with my code or some other issue I'm not able to find at the moment.
FYI, selectedGroup is an array with objects inside, just like this:
[{label: "somelabel", value: 100}]
and there can be many of them, depends on the user.
code:
import React, { useState, useEffect } from 'react';
const GroupButtonMaker = ({ selectedGroup}) => {
const [newButtons, setNewButtons] = useState([]);
console.log(newButtons);
useEffect(() => {
const createButtons = () => {
setNewButtons(
selectedGroup.map(item => {
return (
<button
id={item.value}
className={'btn green micro'}
key={item.value}
onClick={event => btnHandler(event)}
>
{item.label}
</button>
);
})
);
};
createButtons();
}, [selectedGroup]);
const btnHandler = event => {
//here at the handler I'm trying multiple different ways to find indexOf as a test. No luck so far.
const eventID = event.currentTarget.id;
let currentTargetIndex = newButtons
.map(item => item.value)
.indexOf(eventID);
console.log(currentTargetIndex);
console.log(newButtons.findIndex(x => x.value === eventID));
};
Array.prototype.indexOfObject = function arrayObjectIndexOf(property, value) {
for (var i = 0, len = this.length; i < len; i++) {
if (this[i][property] === value) return i;
}
return -1;
};
// here i've hardcored one value as a test to see if it works but it didn't so far.
console.log(newButtons.indexOfObject('value', 107));
const idx = newButtons.reduce(function(cur, val, index, eventID) {
if (val.value === eventID && cur === -1) {
return index;
}
return cur;
}, -1);
console.log(idx);
return <ul>{newButtons.map(item => item)}</ul>;
};
export default GroupButtonMaker;
Thank you beforehand for any suggestions to my current problem. Hopefully I've managed to describe the problem in a way that makes it solveable. If not, please ask and I'll try to provide an answer.
Why not simply pass the id of the button to the handler instead of getting it from event.
You can achieve it by this: onClick={(event) => btnHandler(item.value)}
And then in your btnHandler, just look up the index of the selected button from selectedGroup instead of newButtons.
Here, give this a try:
import React, { useState, useEffect } from "react";
const GroupButtonMaker = ({ selectedGroup }) => {
const [newButtons, setNewButtons] = useState([]);
useEffect(() => {
const buttons = selectedGroup.map(item => {
return (
<button
id={item.value}
className={"btn green micro"}
key={item.value}
onClick={(event) => btnHandler(item.value)}
>
{item.label}
</button>
);
});
setNewButtons(buttons);
}, [selectedGroup]);
const btnHandler = buttonId => {
const selectedButtonIndex = selectedGroup.findIndex(item => item.value === buttonId);
console.log("selectedButtonIndex is: ", selectedButtonIndex);
};
return <ul>{newButtons.map(item => item)}</ul>;
};
export default GroupButtonMaker;
Here's a Working Sample Code Demo for your ref.

Wrong UI Component Deleted When Validating User Input React

essentially I have a KanbanBoard-ish app I'm trying to develop and I'm getting some strange behavior when I call my delete function from my validation function. The code is here on codesandbox. The main issue is that when there are multiple cards and I try to delete a card with an onBlur event, the card where the event occurs is not deleted but another empty card is. It works as expected if all other cards in a column have text. Please ignore the dnd code, as it came after the original problem.
Thanks in advance.
EDIT:
Here is the logic with App.js
state = { list: list }
handleChange = (e, col) => {
let eid = parseInt(e.target.id)
let updatedList = this.state.list.map(obj => {
if (obj.id === col) {
let card = { text: e.target.value }
obj.cards[eid] = card
}
return obj
})
this.setState({ list: updatedList })
}
setText = (e, col) => {
if (e.target.value === "") {
e.target.placeholder = "YOU MUST ENTER TEXT. THIS BOX WILL CLOSE NOW"
e.persist()
setTimeout(() => {
this.delete(e, col)
}, 3000)
return
}
}
delete = (e, col) => {
e.preventDefault()
let eid = parseInt(e.target.id)
let updatedList = this.state.list.map(obj => {
if (obj.id === col) {
obj.cards = obj.cards.filter((c,i) => i !== eid)
//obj.counter--
}
return obj
})
this.setState({ list: updatedList })
}
add = e => {
e.preventDefault()
let eid = parseInt(e.target.id)
let updatedList = this.state.list.map(obj => {
if (obj.id === eid) {
let card = {text:""}
obj.cards.push(card)
obj.counter++
}
return obj
})
this.setState({ list: updatedList })
}
}
map returns an item for each item it iterates through. Maybe using filter would help in the case. I'm assuming that your splice is making the order of this.state.list get crazy and confusing.
let updatedList = this.state.list.filter(obj => obj.id !== col);
Not sure if col or eid is the correct thing to compare to, but that will get you a new list with all of the previous items except for the one whose id matches the id you're trying to delete.
Glancing at your codesandbox, there are some issues. To boil it down to a high level - each card should have an immutable ID, that you can use to delete it. You're using the index of the card in an array and combined with who knows what else. You've lost your source of truth, which is extra important when you are allowing the user to alter the order of an array. Your card should fire the delete function you pass it from its parent. It should just take the id of that card, filter that out of the current state, and set the new state. You're making this overcomplicated.
Parent -
state = { list : [{id: 1, ...other card stuff}, {...more cards}] };
delete = id => {
const newList = this.state.list.filter(item => item.id !== id);
this.setState({ list: newList };
}
render = () => {
const { list } = this.state;
return list.map(item => (
<Card
{...item}
onDelete={this.delete}
/>
))
}
Card -
// whenever you need to delete this card
this.props.onDelete(this.id);

Categories