I was trying to filter an array everytime a letter is entered into a searchbar but I was having trouble retaining the original array when I want to find another item. More specifically, the first time I try to filter using the searchbar it works but when I clear the searchbar and try to look for another item, the filtered array from the first time is still there. I know it is an issue with my useState hook but I'm not sure how to fix it. I also know I need to somehow save a copy of the unfiltered array before it is filtered but again I am not sure how to do that. Any help would be greatly appreciated. Thank you.
import './App.css';
import { useEffect, useState } from 'react';
function App() {
const [persons, setPersons] = useState([
{ name: 'Arto Hellas', phoneNum: '040-123456', id: 1 },
{ name: 'Ada Lovelace', phoneNum: '39-44-5323523', id: 2 },
{ name: 'Dan Abramov', phoneNum: '12-43-234345', id: 3 },
{ name: 'Mary Poppendieck', phoneNum: '39-23-6423122', id: 4 }
])
const [newName, setNewName] = useState('')
const [newPhone, setNewPhone] = useState('');
const [searchVal, setSearchVal] = useState('');
const handleChange = (e) => {
console.log(e.target.value);
setNewName(e.target.value);
}
const handleSubmit = (e) => {
console.log("Clicked submit btn")
e.preventDefault();
const newPerson = {
name: newName,
phoneNum: newPhone
};
if(persons.filter((person) => { return person.name === newPerson.name}).length > 0){
alert(`${newName} already exists. Try another name.`);
}
else {
setPersons(persons.concat(newPerson));
console.log("persons array is now",persons)
}
setNewName("");
setNewPhone("");
}
const searchLogic = (e) => {
console.log("searchVal is",e.target.value);
setSearchVal(e.target.value); //everytime the user selects a letter - the search value gets updated
}
useEffect(() => {
//filters persons array by the user-entered letters(searchVal)
const filtered = persons.filter((person) => {
return person.name.startsWith(searchVal); //array with matching results
})
if(filtered.length > 0){
setPersons(filtered);
}
else {
console.log(`We could not find a user by the name of ${searchVal}`)
}
},[searchVal])
//Works well for the first time, but the second time I try to look for another item, the old filtered array is still there
return (
<div>
<h2>Phonebook</h2>
<div className="searchfield">
filter shown with a <input type="text" value={searchVal} onChange={searchLogic}/>
</div>
<form onSubmit={handleSubmit}>
<div>
name: <input value={newName} onChange={handleChange}/>
</div>
<div>
number: <input value={newPhone} onChange={(e) => setNewPhone(e.target.value)}/>
</div>
<div>
<button type="submit">add</button>
</div>
</form>
<h2>Numbers</h2>
{persons.map((person) => <p key={person.name}>{person.name} ---- {person.phoneNum}</p>)}
</div>
);
}
export default App;
CodePudding user response:
Every change on searchVal you're filtering the state and then setting it, making it shorter every time. What you wanna do here is create filtered as a derivated data from persons state and do a map on this data.
{filtered.map((person) => <p key={person.name}>{person.name} ---- {person.phoneNum}</p>)}
You can also use a useMemo here to skip some unrelated renders
const filtered = useMemo(() =>
persons.filter((person) =>
person.name.startsWith(searchVal);
), [searchVal, persons]);
I'd rename filtered to a more descriptive name and there also a small problem here:
setPersons(persons.concat(newPerson));
console.log("persons array is now",persons)
setState is async and here you're logging just after you call it, making it return an old state, if you wanna see most updated state value you need to run a console.log inside a useEffect that has state as a dependency.
CodePudding user response:
Instead of having a useEffect
which overwrites your state with the filtered array, keep the full array in state the whole time. The filtered array is a derived value, which can be calculated based on the full array and the search term during rendering.
const [persons, setPersons] = useState([
{ name: 'Arto Hellas', phoneNum: '040-123456', id: 1 },
{ name: 'Ada Lovelace', phoneNum: '39-44-5323523', id: 2 },
{ name: 'Dan Abramov', phoneNum: '12-43-234345', id: 3 },
{ name: 'Mary Poppendieck', phoneNum: '39-23-6423122', id: 4 }
])
// ...
const [searchVal, setSearchVal] = useState('');
// ... no useEffect
const filtered = persons.filter((person) => {
return person.name.startsWith(searchVal);
})
return (
<div>
// ...
{filtered.map((person) => (
<p key={person.name}>
{person.name} ---- {person.phoneNum}
</p>
))}
</div>
);
To improve performance, useMemo
can be used to skip the calculation if nothing has changed.
const filtered = useMemo(() => {
return persons.filter((person) => {
return person.name.startsWith(searchVal);
})
}, [searchVal, persons]);
CodePudding user response:
A pretty simple solution would be to create a new variable through useState
which always contains your filtered persons
array. This way you keep persons
as your "repository" and, let's say, filteredPersons
as your displayed data, even when you don't have anything to filter on.
So your code could look like :
const [filteredPersons, setFilteredPersons] = useState(persons);
...
useEffect(() => {
//filters persons array by the user-entered letters(searchVal)
const filtered = persons.filter((person) => {
return person.name.startsWith(searchVal); //array with matching results
})
setFilteredPersons(filtered);
if(filtered.length == 0){
console.log(`We could not find a user by the name of ${searchVal}`)
}
},[searchVal, persons])
And then use filteredPersons
in your render function.
Note : Since your effect is dependent on persons, you must add it to the array of dependencies of useEffect
Hope this helps!