I'm having trouble understanding why a list won't update in React. For my website that I'm building, I'm trying to add a 'favorites' button, but when you click the button it updates the state but the changes never re-render in the list. I tried to make a simpler version, but this doesn't work either:
import React, { useState } from 'react';
import './App.css';
function App() {
const [favorites, setFavorites] = useState([]);
function addFavorite(name, id) {
let newFavorites = favorites;
let newFav = {name: name, id: id};
newFavorites.push(newFav);
setFavorites(newFavorites);
}
return (
<div className="App">
<ul>
{favorites.map((val) => {
return(<li key={val.id}><span>{val.name}</span></li>);
})}
</ul>
<button onClick={() => addFavorite("Thing1", 1)}>Thing 1</button>
<button onClick={() => addFavorite("Thing2", 2)}>Thing 2</button>
<button onClick={() => {console.log(favorites)}}>Check</button>
</div>
);
}
export default App;
I can see the state changes in the console when I log them, but the <ul> element never updates. I've looked online but most of the articles I've found have not been very helpful (I feel the example code I wrote looks a lot like this article.
let newFavorites = favorites;
This assigns newFavorites to point to favorites
newFavorites.push(newFav);
Because newFavorites points to favorites, which is an array in state, you can't push anything onto it and have that change render.
What you need to do, is populate a new array newFavorites with the content of favorites.
Try
const newFavorites = [...favorites];
That should work
I would make some changes in your addFavourite function:
function addFavorite(name, id) {
let newFav = {name, id};
setFavorites([…favourites, newFav]);
}
This way, everytime you click favourite, you ensure a new array is being created with spread operator
Its not working because use are mutating the existing state.
The list is updating but it won't render as useState only renders when the parameter passed to it is different from previous one but in your case though you are changing the list items still the reference is not altering.
To make it work you can use spread operator for lists for even Array.concat() returns a new updated array.
function addFavorite(name, id) {
let newFav = {name: name, id: id};
setFavorites(prev=>[...prev, newFav]);
}
For changing array state, you should use:
function addFavorite(name, id) {
let newFav = { name: name, id: id };
setFavorites((favorites) => [...favorites, newFav]);
}
Related
Need help with the REACT code below. Making a note taking app and tried to make it so that a new note appears at the top of the list of my notes. My code works fine when I add the new note at the end of the array, but if I switch it so that when I add it in the beginning like so:
const newNotes = [newNote, ...notes];
it then displays all the same old notes with the last one repeated twice. the code responsible for displaying the notes on the screen looks like this.
const listNotes = ()=>{
console.log(notes);
return notes.map((obj, index) => (
<Note
key={index}
id={obj.id}
subject={obj.subject}
text={obj.text}
date={obj.date}
handleDeleteNote={deleteNote}
/>
))
}
After some debugging I notice that the new list is being created successfully and the map is creating the notes components "successfully" and when I print notes to the console right before the map function it seems to show that the the newly created list is fine. but when it gets to the ".map()" for some reason it's not following the list exactly, it's almost like it doesn't realize there's a new item in the list until it's halfway through redering the notes.
full code of component below.
import React, { useState, useEffect } from "react";
import { nanoid } from "nanoid"; //allows for random IDs
import NoteSearch from "./NoteSearch"; // component that searches notes
import Note from "./Note"; // Component for saved notes
import AddNewNote from "./AddNewNote"; //Component to add new note
const NotesList = () => {
// States
const [notes, setNotes] = useState([
{
id: nanoid(),
subject: "Fruit",
text: "bananas are awesome",
date: "9/23/2022",
},
{
id: nanoid(),
subject: "ew",
text: "onions are awesome",
date: "9/24/2022",
},
{
id: nanoid(),
subject: "veggie",
text: "carrots are awesome",
date: "9/25/2022",
},
]);
// Add and Delete Note handlers
const addNote = (subject, text) => {
const date = new Date();
const newNote = {
id: nanoid(),
subject: subject,
text: text,
date: date.toLocaleDateString(),
};
const newNotes = [newNote, ...notes];
setNotes(newNotes);
};
const deleteNote = (CurrentID) => {
const newNotes = notes.filter((note)=>note.id !== CurrentID);
setNotes(newNotes);
};
//functions
const listNotes = ()=>{
console.log(notes);
return notes.map((obj, index) => (
<Note
key={index}
id={obj.id}
subject={obj.subject}
text={obj.text}
date={obj.date}
handleDeleteNote={deleteNote}
/>
))
}
// This is the notes app DOM
return (
<div className="notes-wrapper">
<NoteSearch />
<div className="all-notes">
<AddNewNote handleAddNote={addNote}/>
{
listNotes()
}
</div>
</div>
);
};
Use id as key not index:
<Note
key={obj.id}
id={obj.id}
subject={obj.subject}
text={obj.text}
date={obj.date}
handleDeleteNote={deleteNote}
/>
You are changing your list dynamically and it is discouraged to use index as key and may cause an issue.
React doc.: We don’t recommend using indexes for keys if the order of items may
change.
This is because you are using indexes as keys. React thinks that you don't want to unmount/remount the n first nodes (those that were already existing). It will just update their props. Likely your Node component initializes its state in the constructor or after the component did mount and do not recompute the state on props changes. As a consequence the list is not updated visually, except for the last new node.
Using the id as key should solve the problem.
, Using props I was able to effectively pass state upwards from my child component to its parent, but a change in the state does not cause a re-render of the page.
import React, { useState } from "react";
export default function App() {
const AddToList = (item) => {
setText([...text, item]);
};
const removeFromList = (item) => {
const index = text.indexOf(item);
setText(text.splice(index, 1));
};
const [text, setText] = React.useState(["default", "default1", "default2"]);
return (
<div className="App">
<div>
<button
onClick={() => {
AddToList("hello");
}}
>
Add
</button>
</div>
{text.map((item) => {
return <ChildComponent text={item} removeText={removeFromList} />;
})}
</div>
);
}
const ChildComponent = ({ text, removeText }) => {
return (
<div>
<p>{text}</p>
<button
onClick={() => {
removeText(text);
}}
>
Remove
</button>
</div>
);
};
In the snippet, each time AddToList is called, a new child component is created and the page is re-rendered reflecting that. However, when i call removeFromList on the child component, nothing happens. The page stays the same, even though I thought this would reduce the number of childComponents present on the page. This is the problem I'm facing.
Updated Answer (Following Edits To Original Question)
In light of your edits, the problem is that you are mutating and passing the original array in state back into itself-- React sees that it is receiving a reference to the same object, and does not update. Instead, spread text into a new duplicate array, splice the duplicate array, and pass that into setText:
const removeFromList = (item) => {
const index = text.indexOf(item);
const dupeArray = [...text];
dupeArray.splice(index, 1);
setText(dupeArray);
};
You can see this working in this fiddle
Original Answer
The reason React has things like state hooks is that you leverage them in order to plug into and trigger the React lifecycle. Your problem does not actually have anything to do with a child attempting to update state at a parent. It is that while your AddToList function is properly leveraging React state management:
const AddToList = (item) => {
setText([...text, item]);
};
Your removeFromList function does not use any state hooks:
const removeFromList = (item) => {
const index = text.indexOf(item);
text.splice(index, 1); // you never actually setText...
};
...so React has no idea that state has updated. You should rewrite it as something like:
const removeFromList = (item) => {
const index = text.indexOf(item);
const newList = text.splice(index, 1);
setText(newList);
};
(Also, for what it is worth, you are being a little loose with styles-- your AddToList is all caps using PascalCase while removeFromCase is using camelCase. Typically in JS we reserve PascalCase for classes, and in React we also might leverage it for components and services; we generally would not use it for a method or a variable.)
So I'm still learning React and I'm trying to use it to remove an item from a "to do list".
Here is the code:
import { Item } from '../../types/item';
import { useState } from 'react';
type Props = {
item: Item
}
export const ListItem = ({ item }: Props) => {
const [isChecked, setIsChecked] = useState(item.done);
return (
<C.Container done={isChecked}>
<input
type="checkbox"
checked={isChecked}
onChange={e => setIsChecked(e.target.checked)}
/>
<button onClick={removeItem}><img src="./remove"/></button>
<label>{item.name}</label>
</C.Container>
);
}
This button should call the function removeItem that will... remove the item haha.
Any suggestions?
I suggest you to use a functional component with useState hook. Create a state with initial value to be an empty array. then create a function which on click with update the value in state, it canbe done using useState hook. Simply enter setTodo(). This will create a new entry in your state. You can use array methods to add the returned value from function which is triggered onclick, too delete an entry, simply use a filter method, you can look more about it on w3school, it provides good examples which are easy to understand by biggeners.
I am fetching data from an API using axios.
On my invoice details page when I try to get data of only one invoice using this code
const id = props.match.params.id;
const invoice = useSelector((state) => state.invoices.find(invoice => invoice._id === id));
It returns an object or undefined but I only want an object inside an array or an empty array not undefined how should I do that?
When I tried to use .filter method instead of .find, it logged the array into the console infinite time.
Complete code:
import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import backIcon from '../assets/images/icon-arrow-left.svg'
import InvoiceDetailsHeader from './InvoiceDetailsHeader';
import { useSelector } from 'react-redux';
// remove this after adding DB
import data from '../data.json'
import InvoiceCardDetails from './InvoiceCardDetails';
const InvoiceDetails = (props) => {
const [invoiceData, setInvoiceData] = useState([]);
const id = props.match.params.id;
const invoice = useSelector((state) => state.invoices.find(invoice => invoice._id === id));
useEffect(() => {
setInvoiceData(invoice);
// console.log(invoiceData)
}, [id, invoice]);
return (
<div className="mx-auto px-12 py-16 w-full max-w-3xl">
<Link to="/" className="text-neutral text-xs"><img className="inline -mt-1 mr-4" src={backIcon} alt="back" /> Go back</Link>
<InvoiceDetailsHeader data={invoiceData} />
<InvoiceCardDetails data={invoiceData} />
</div>
)
}
export default InvoiceDetails
Anyone please help me with this.
I think it's because you're setting setInvoiceData(invoice) which is undefined at the very start. so make a check on it
if(invoice){
setInvoiceData([invoice])
}
please try this one
useEffect(() => {
if(invoice){
setInvoiceData([...invoiceData, invoice])
}
}, [id, invoice]);
First of all, I don't know if I missed anything, but I don't think it's a good way for invoice to be possible for both objects and empty array. I think a better way is to divide the conditions and render the invoice when the ID is not found.
If a filter method is used instead of a find, the filter method returns a new array instance each time. So as the second argument(invoice) of use Effect changes, the update callback of use Effect will continue to be repeated.
const invoice = useSelector((state) => state.invoices.find(invoice => invoice._id === id) ?? []);
What you want can be done simply using Nullish coalescing operator.
However, [] also creates a new array instance, so update callback is repeated indefinitely.
So to make what you want work in the current code, please remove the invoice from the dependence of useEffect as below.
useEffect(() => {
setInvoiceData(invoice);
// console.log(invoiceData)
}, [id]);
From what I learnt about React, you should not mutate any objects otherwise React doesn't know to re-render, for example, the following example should not trigger re-render in the UI when button is clicked on:
import React, { useState } from "react";
import ReactDOM from "react-dom";
function App({ input }) {
const [items, setItems] = useState(input);
return (
<div>
{items.map((item) => (
<MyItem item={item}/>
))}
<button
onClick={() => {
setItems((prevItems) => {
return prevItems.map((item) => {
if (item.id === 2) {
item.name = Math.random();
}
return item;
});
});
}}
>
Update wouldn't work due to shallow copy
</button>
</div>
);
}
function MyItem ({item}) {
const name = item.name
return <p>{name}</p>
}
ReactDOM.render(
<App
input={[
{ name: "apple", id: 1 },
{ name: "banana", id: 2 }
]}
/>,
document.getElementById("container")
);
You can try above code here
And the correct way to update the array of objects should be like below (other ways of deep copying would work too)
setItems((prevItems) => {
return prevItems.map((item) => {
if (item.id === 2) {
# This way we return a deepcopy of item
return {...item, name: Math.random()}
}
return item;
});
});
Why does the 1st version works fine and the UI is updated right away even though I'm just updating the original item object?
Render happens due to .map that creates new array. If you do something like prev[1].name = "x"; return prev; in your hook, this will not perform an update. Per reactjs doc on setState with function argument:
If your update function returns the exact same value as the current
state, the subsequent rerender will be skipped completely.
Update.
Yes, speaking of parent-child interaction, item would be the same (by reference), but the child props would differ. You have MyItem({ item }) and this item is being destructured from props, say MyItem(props), and this props changes because of the parent source is changed.
So each time you map the list, you explicitly ask the parent to render its children, and the fact that some (or all) of the child's param is not changed does not matter. To prove this you may remove any params from the child component:
{items.map(() => ( <MyItem /> ))}
function MyItem () {
return <p>hello</p>
}
MyItem will be invoked each time you perform items update via state hook. And its props will always be different from the previous version.
if your setItem setting a new object, your page with a state change will re-render
in your logic, you performed a shallow copy, which:
a shallow copy created a new object, and copy everything of the 1st level from old object.
Shallow copy and deep copy also created a new object, they both trigger a re-render in React.
The different of shallow copy and deep copy is: from 2nd level of the old object, shallow copy remains the same objects, which deep copy will create new objects in all level.