`setState` doesn't update array - javascript

I have a component that renders an array with .map() and a button that setState the shuffled version of it.
I'm trying to have it update every time I click that button but it doesn't.
const Home: NextPage = () => {
const [random, setRandom] = useState(students);
function randomize() {
const shuffle = students.sort(() => 0.5 - Math.random());
setRandom(shuffle);
}
return (
<>
<main className="container mx-auto flex min-h-screen flex-col items-center justify-center p-4">
<h1 className="text-6xl font-bold">This week&apos;s seat</h1>
<button onClick={randomize}>Shuffle</button>
{/* Seatings */}
<div className="seatbox">
{random.map((student) => (
<Seat key={student}>{student}</Seat>
))}
</div>
</main>
</>
);
};
I tried to log the random state, it got updated it just doesn't get rendered probably.
The Seat component is made with styled-components and the students array is an array of strings with the students name like this
const students = [
"Joe",
"Jason"
]

This sorts the array:
const shuffle = students.sort(() => 0.5 - Math.random());
But... it sorts the array in place and returns a reference to that same array. Since the reference equality hasn't changed (an array is a kind of object after all), React has no way to know that the state has been updated. It doesn't do any kind of deep compare, just an equality comparison for whatever is sent to it.
If you duplicate the array just before sorting (so as to not mutate it), the newly sorted array will be a new object reference and won't be "equal" to the previous state:
const shuffle = [...students].sort(() => 0.5 - Math.random());

Related

Generate a new item in an array in React?

I'm building a to-do list and I want each list item to have a number, starting from 1. I'm using the useState hook on the counter but I can't figure out how to add new items to the array every time I click on a button. I haven't coded in months and I'm really rusty.
function Product() {
const [input, setInput] = useState("");
const [todo, setTodo] = useState([]);
const [count, setCount] = useState([]);
const addTodo = e => {
e.preventDefault();
setTodo([...todo, input]);
setCount(prevCount => [...prevCount, prevCount + 1]);
setInput("");
};
return (
<div>
<form>
<input value={input} onChange={e => setInput(e.target.value)} type='text' />
<button type='submit' onClick={addTodo}>
Add!
</button>
</form>
<h2 style={{ marginBottom: "0.5rem" }}>List of To-dos !</h2>
{todo.map(todo => (
<p>
{count.map(count => (
<p>{count}</p>
))}
{todo}
</p>
))}
</div>
);
}
I want to make it so each time I add a list item, it adds its number to the left. The first item would have 1, the second 2, etc..
This is really bad practice:
What you want to do is use the index param that Javascript map function gives you:
Good practice
todo.map((todo, index) => (
<p>{index} - {todo}</p>
))
Output
0 - walk dog
1 - do yoga
Now if want the index to start at 1, you can simply add +1 to index
todo.map((todo, index + 1) => (
<p>{index} - {todo}</p>
))
Output
1 - walk dog
2 - do yoga
Since the index values are unique, you could use that to your benefit when performing other actions such as deleting etc. Usually you add the key attribute to the individual child values according to the official React documentation as follows
todo.map((todo, index + 1) => (
<p key={index + 1}>{index} - {todo}</p>
))
where key is a unique value.
Also, change your variable names to something more meaningful.
I'd suggest changing todo to plurial todos.
Your final code should look like this:
function Product() {
const [input, setInput] = useState("");
const [todo, setTodo] = useState([]);
const addTodo = e => {
e.preventDefault();
setTodo([...todo, input]);
setInput("");
};
return (
<div>
<form>
<input value={input} onChange={e => setInput(e.target.value)} type='text' />
<button type='submit' onClick={addTodo}>
Add!
</button>
</form>
<h2 style={{ marginBottom: "0.5rem" }}>List of To-dos !</h2>
{todo.map((count, index + 1) => (
<p key={index + 1}>{index} {todo}</p>
))}
</div>
);
}
Don't.
There's no reason to track the count of items in an array as its own separate state value.
Arrays have a length property which tells you the count of items in the array.
.map() includes an index in the callback, so if you just want to output the array index then you can do that.
You certainly don't need an array of numbers from 1-X in order to know the numbers from 1 to X. The value of X alone gives you this information.
Remove the count state value entirely, and just output the index of the array from the todo list:
{todo.map((t, x) => (
<p>
{x}
{t}
</p>
))}
Note also that I abbreviated your inner todo variable to just t. You can call it whatever you like, but giving it the same name as another variable you already have is just asking for confusion and bugs.
You may instead want to rename your array to something plural, like todos. Then each item therein is semantically a todo:
{todos.map((todo, x) => (
<p>
{x}
{todo}
</p>
))}
Basically, names are important. A variable/type/property/etc. name should tell you exactly and unambiguously what it is. Poor variable names lead to confusing code, and being confused about your code is exactly what brought you here.

What should I use as a key for "row" elements in react?

I have a gallery that displays a number of books per row. This gallery takes an array of books as a prop and uses "itemsPerRow" prop to chunk the books into a 2 dimensional array and then loops through all the books to display the books in a grid-like structure.
export default function Gallery({ items, itemsPerRow, renderLink }) {
itemsPerRow = itemsPerRow ?? 3
const rows = chunk(items, itemsPerRow)
const renderEmptyItems = (itemsToRender) => {
const items = []
for(let n = itemsToRender; n > 0; n--) {
items.push(<GalleryItem key={`empty_${n}`} empty/>)
}
return items
}
return (
<div>
{
rows.map((row, index) => (
<div key={index} className="tile is-ancestor">
{row.map(item => <GalleryItem key={item.id} renderLink={renderLink} {...item}/>)}
{/* Add empty gallery items to make rows even */}
{index + 1 === rows.length && renderEmptyItems(itemsPerRow - row.length)}
</div>
))
}
</div>
)
}
However, unless I give each div representing a row a key, react complains about the lack of keys. As I understand it, using the index as a key doesn't really help react and should be avoided. So what should I use as a key here <div key={index} className="tile is-ancestor"> instead of the index?
Use a unique identifier (book.id, maybe book.title if it's unique) for the key props. If your data does not have a unique identifier, it's okay to use index.
You need to specify a value that uniquely identify the item, such as the id. You can read more about keys in the documentation.
Also it is not recommended to use indexes as keys if the order of your data can change, as React relies on the keys to know which components to re-render, the documentation I linked explains that further.
You can use the unique_identifier which differentiate each of the documents(probably, you should pass the document _id as a key prop in the row components)
<div className="row">
{notes.map((item) => {
return (
<div key={note._id} className="col-md-6">
<Component item={item} />
</div>
);
})}
</div>

index with lodash times method

I need to replicate a component "n" times. To do that I used the lodash method times. The problem is that I need an index as a key for the components generated and It doesn't look like it has one.
I have the following code:
export const MyComponent: React.FC<{times: number}> = ({ times }) => {
return (
<>
{_.times(times, () => (
//I need a key={index} in this div
<div className="bg-white border-4 border-white md:rounded-md md:p-2 content-center my-4 shadow w-full">
</div>
))}
</>
);
};
This will return the component that is inside n times.
I tried to do a method that returns the component and set an index with useState, but it goes in an infinite loop. I thought to put a big random number as a key so it is extremely difficult to get the same, but I don't like that solution. I'd like to use this method because it is clean.
So what do you think I could do to give a to the component?
It's passed to you as a function parameter:
_.times(times, (index) => (blabla))

Comparing Arrays using .map (ReactJS)

In my project, I have 2 arrays being read from Firestore.
courseList - A list of courses of which a user is enrolled in
courses - A list of all of the courses available in the project
I would like to compare these using a .map so that in my course portal, only the courses of which the user is enrolled in is rendered.
Here is what the arrays look like:
courseList:
courses
I know the arrays work, however, the .map doesn't seem to be working!
Here's my code:
const {courses} = this.state
const {courseList} = this.state
{
courses.length && courses.map (course => {
if (course.courseUrl === courseList.CourseCode) {
return (
<div className = "CourseTile" key = {course.courseName}>
<div className = "CourseTitle">
<h1> {course.courseName}</h1>
</div>
<div className = "CourseDescription">
<p> {course.courseSummary}</p>
</div>
<Link to={`/course/${course.courseUrl}/courseinformation`}> <button className = "LetsGoButton"> Take course</button> </Link>
</div>
)
}
else return null;
}
)
}
If I replace
if (course.courseUrl === courseList.CourseCode)
with
if (course.courseUrl === "websitedesign")
It renders the website design course only, So I believe there's something wrong with this line.
Any help would be appreciated.
You are correct in where the problem lies:
course.courseUrl === courseList.CourseCode
In this case course is a single item from a list, with a property courseUrl. That's fine. But courseList is an array of items, each of which has a CourseCode property. The Array itself does not (although, interestingly, it could).
It seems like what you are trying to do is pull the full course data (from courses) but filtered to only the ones the user has. In this case, you have to loop through one list, looking through the other list for each item. What you want is filter (or, more powerfully, reduce) but probably not map.
const filteredCourses = availableCourses.filter( availableCourse => studentsCourses.some( studentsCourse => studentsCourse.id === availableCourse.id ) );
You'll notice I renamed the variables to make it clear which of the two lists is being used at each part.
The outer function filter will return a new array containing only those items that return 'true' in the callback function.
The inner callback function some loops through another array (the student's enrolled courses) and returns true if it finds any that match the given condition.
So in English, "Filter this list of all the courses, giving me back only the courses that have a matching ID in the list of the student's enrolled courses."
if (course.courseUrl === courseList.CourseCode)
You can filter the courses array by checking if each course is included in the courseList array, matching an URL to a courseList element's CourseCode property. array.prototype.some is used to iterate the course list and check that at least one courseList item matches. Once filtered you can map the filtered result as per normal.
const {courses} = this.state;
const {courseList} = this.state;
...
{courses
.filter(({ courseUrl }) =>
courseList.some(({ CourseCode }) => CourseCode === courseUrl)
)
.map((course) => {
return (
<div className="CourseTile" key={course.courseName}>
<div className="CourseTitle">
<h1> {course.courseName}</h1>
</div>
<div className="CourseDescription">
<p> {course.courseSummary}</p>
</div>
<Link to={`/course/${course.courseUrl}/courseinformation`}>
{" "}
<button className="LetsGoButton"> Take course</button>{" "}
</Link>
</div>
);
})}

ReactJS - How to remove an item from an array in a functional component?

I am trying to create a simple shopping cart using ReactJS and I figured a potential way out but whenever I click on the remove button I've set it doesn't really remove the items from the cart..
So those are my state managers right here:
let[product, setProduct] = useState([])
//The function bellow is what I use to render the products to the user
const[item] = useState([{
name: 'Burger',
image: '/static/media/Burger.bcd6f0a3.png',
id: 0,
price: 16.00
},
{
name: 'Pizza',
image: '/static/media/Pizza.07b5b3c1.png',
id: 1,
price: 20.00
}])
and I have a function that adds the objects in item to the product array, then I have a function that is supposed to remove them that looks like this:
const removeItem=(idx)=>
{
// let newProduct = product.splice(idx,1)
// setProduct([product,newProduct])
// $('.showItems').text(product.length)
// product[idx]=[]
product.splice(idx,1)
if(product.length<=0)
{
$('.yourCart').hide()
}
}
This function is called from here:
{product.map((item, idx)=>
<div className='yourCart' key={idx}>
<hr></hr>
<div>
<img src ={item.image}></img>
<h3 className='burgerTitle'>{item.name}</h3>
<h4><strong>$ {item.price}.00</strong></h4>
<Button variant='danger' onClick={()=>removeItem(idx)}>Remove</Button>
</div>
<br></br>
</div>)}
The problem is that I've tried to use splice, setState, I tried to even clear the entire array and add the elements that are left after applying the filter function to it but it was all to no avail.
How can I make it so that when I click on the remove button it removes the specific item from the array??
You need to use the mutation method setProduct provided from the useState hook to mutate product state.
const removeItem = (id) => {
const index = product.findIndex(prod => prod.id === id); //use id instead of index
if (index > -1) { //make sure you found it
setProduct(prevState => prevState.splice(index, 1));
}
}
usage
<Button variant='danger' onClick={()=>removeItem(item.id)}>Remove</Button>
as a side note:
Consider using definite id values when working with items in an array, instead of index in array. the index of items can change. Use the item.id for a key instead of the index when mapping. Consider using guids as identification.
{product.map((item, idx)=>
<div className='yourCart' key={`cartItem_${item.id}`}> //<-- here
<hr></hr>
<div>
<img src ={item.image}></img>
<h3 className='burgerTitle'>{item.name}</h3>
<h4><strong>$ {item.price}.00</strong></h4>
<Button variant='danger' onClick={()=>removeItem(item.id)}>Remove</Button>
</div>
<br></br>
</div>)}
You can define removeItem as a function, which gets an id (instead of an index, since that's safer) and setProduct to the subset which should remain. This could be achieved in many ways, in this specific example I use .filter() to find the subset of product whose elements differ in their id from the one that is to be removed and set the result as the new value of product.
removeItem = (id) => {
setProduct(product.filter((i)=>(i.id !== id)))
}

Categories