Only show matching string in array when using 'useMemo' in react - javascript

I have an autocomplete input field and running into issues when initially hiding the output in my array. Instead of showing all my items when mounted, I just want to show the matching string. Leaving my state empty const [filteredRecipes, setFilteredRecipes] = useState(); will throw in error in my loop.
Codesanbox:
https://codesandbox.io/s/quzwg?file=/App.js:251-313
Js:
export default function App() {
const items = useMemo(
() => ["item1", "item2", "anotheritem", "yetanotheritem", "myitem"],
[]
);
const [search, setSearch] = useState("");
const [filteredRecipes, setFilteredRecipes] = useState(items);
const handleSearchChange = event => {
setSearch(event.target.value);
};
useEffect(() => {
setFilteredRecipes(
items.filter(el => el.toLowerCase().includes(search.toLowerCase()))
);
}, [search, items]);
return (
<div>
<input
type="search"
placeholder="type here to filter"
value={search}
onChange={handleSearchChange}
/>
<div>
{filteredRecipes.map(recipe => (
<p>{recipe}</p>
))}
</div>
</div>
);
}

If I'm understanding your question correctly, in your example filteredRecipes are the autocomplete suggestions that you want to initially hide when mounting, or, making an assumption here, whenever the search value is falsey. You can do this by conditionally filtering on search state being truthy/falsey. All strings will include the empty string (''), so you want to handle this case differently.
setFilteredRecipes(
search
? items.filter((el) => el.toLowerCase().includes(search.toLowerCase()))
: []
);
Code
export default function App() {
const items = useMemo(
() => ["item1", "item2", "anotheritem", "yetanotheritem", "myitem"],
[]
);
const [search, setSearch] = useState("");
const [filteredRecipes, setFilteredRecipes] = useState([]);
const handleSearchChange = (event) => {
setSearch(event.target.value);
};
useEffect(() => {
setFilteredRecipes(
search
? items.filter((el) => el.toLowerCase().includes(search.toLowerCase()))
: []
);
}, [search, items]);
return (
<div>
<input
type="search"
placeholder="type here to filter"
value={search}
onChange={handleSearchChange}
/>
<div>
{filteredRecipes.map((recipe) => (
<p>{recipe}</p>
))}
</div>
</div>
);
}

Related

Why the filter does not return the list on the initial render?

What I have is a list that was fetched from an api. This list will be filtered based on the input. But at the first render it will render nothing, unless I press space or add anything to the input. Another solution is set the fetched data to the filteredList. But I don't know if it is the right thing to set the fetched data to two arrays.
import React, { useState, useEffect } from "react";
const PersonDetail = ({ person }) => {
return (
<div>
Id: {person.id} <br />
Name: {person.name} <br />
Phone: {person.phone}
</div>
);
};
const App = () => {
const [personsList, setPersonsList] = useState([]);
const [personObj, setPersonObj] = useState({});
const [showPersonDetail, setShowPersonDetail] = useState(false);
const [newPerson, setNewPerson] = useState("");
const [filter, setFilter] = useState("");
const [filteredList, setFilteredList] = useState(personsList);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setPersonsList(data);
//setFilteredList(data) <-- I have to add this to work
console.log(data);
});
}, []);
const handleClick = ({ person }) => {
setPersonObj(person);
if (!showPersonDetail) {
setShowPersonDetail(!showPersonDetail);
}
};
const handleChange = (event) => {
setNewPerson(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
const tempPersonObj = {
name: newPerson,
phone: "123-456-7890",
id: personsList.length + 1,
};
setPersonsList((personsList) => [...personsList, tempPersonObj]);
//setFilteredList(personsList) <-- to render the list again when add new person
setNewPerson(" ");
};
const handleFilter = (event) => {
setFilter(event.target.value);
const filteredList =
event.target.value.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(event.target.value.toLowerCase())
)
: personsList;
setFilteredList(filteredList);
};
return (
<div>
<h2>List:</h2>
Filter{" "}
<input value={filter} onChange={handleFilter} placeholder="Enter" />
<ul>
{filteredList.map((person) => {
return (
<li key={person.id}>
{person.name} {""}
<button onClick={() => handleClick({ person })}>View</button>
</li>
);
})}
</ul>
<form onSubmit={handleSubmit}>
<input
placeholder="Add Person"
value={newPerson}
onChange={handleChange}
/>
<button type="submit">Add</button>
</form>
{showPersonDetail && <PersonDetail person={personObj} />}
</div>
);
};
export default App;
Your filtered list is actually something derived from the full persons list.
To express this, you should not create two apparently independent states in this situation.
When your asynchronous fetch completes, the filter is probably already set and you are just setting personsList which is not the list you are rendering. You are rendering filteredList which is still empty and you are not updating it anywhere, except when the filter gets changed.
To avoid all of this, you could create the filtered list on each rendering and — if you think this is not efficient enough — memoize the result.
const filteredList = useMemo(() =>
filter.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(filter.toLowerCase())
)
: personsList,
[filter, personsList]
);
When the filter input gets changed, you should just call setFilter(event.target.value).
This way, you will always have a filtered list, independent of when your asynchronous person list fetching completes or when filters get updated.
Side note: Writing const [filteredList, setFilteredList] = useState(personsList); looks nice but is the same as const [filteredList, setFilteredList] = useState([]); because the initial value will be written to the state only once, at that's when the component gets initialized. At that time personsList is just an empty array.

React.js: filtering API object with 2 search bars

I am fetching an API data set and filtering that data with a search bar to locate by first or last name. I also have an input field that allows you to add "tags" to the data set that I am mapping through. I am trying to add a second search bar to filter the original data by the unique tags as well, but can not figure out how to incorporate that information into the filter.
export default function Home() {
const [students, setStudents] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
const [showTests, setShowTests] = useState({});
const [tagSearch, setTagSearch] = useState("");
const [tags, setTags] = useState([]);
useEffect(() => {
const getData = async () => {
try {
const response = await axios.get(
<!-- API -->
);
setStudents(response.data);
setError(null);
} catch (err) {
setError(err.message);
setStudents(null);
} finally {
setLoading(false);
}
};
getData();
}, []);
return (
<div className="home-main">
<Search setSearch={setSearch} />
<TagSearch setTagSearch={setTagSearch} />
{loading && <div>Loading, please wait ...</div>}
{error && (
<div>{`An Error has occurred. - ${error}`}</div>
)}
<div className="students">
<Fragment>
{
students
&&
students.students.filter((val) => {
if(search === '' || tagSearch === '') {
return val
} else if(val.firstName.toLowerCase().includes(search.toLowerCase())
|| val.lastName.toLowerCase().includes(search.toLowerCase())
|| tags.text.toLowerCase().includes(tagSearch.toLowerCase()) ){
return val
}
}).map(({val}) => (
<!-- additional info -->
<div className="tags">
<Tags setTags={setTags} />
</div>
</div>
</div>
))
}
</Fragment>
</div>
</div>
);
}
This is where the "tag" state is coming from...
export default function Tags({setTags}) {
const [inputText, setInputText] = useState('');
const [tiles, setTiles] = useState([]);
const inputTextHandler = (e) => {
setInputText(e.target.value);
};
const submitTagHandler = () => {
setTiles([
...tiles, {text: inputText, id: Math.floor(Math.random() * 1000000)}
]);
setTags([
...tiles, {text: inputText}
])
setInputText('');
};
return (
<div className="tags-main">
<div className="tiles-contain">
{
tiles.map((obj) => (
<Tiles key={obj.id} text={obj.text} id={obj.id} tiles={tiles} setTiles={setTiles} />
))
}
</div>
<input value={inputText} onChange={inputTextHandler} onKeyPress={(e) => {
if(e.key === 'Enter') {
if(inputText !== "") {
submitTagHandler();
} else {
alert("Please enter a tag")
}
};
}} placeholder='Add Tag Here' type="text" />
</div>
);
}
It works without the tag state added to the filter. After adding the tag logic neither search bar works. How can I add the array of tags to the filter dependency to sort by first or last name and tags?
I'm pretty sure you were getting an error "cannot read toLowerCase of undefined"
You probably wanted to do something like this
tags.some(tag => tag.text.toLowerCase() === tagSearch.toLowerCase())
or
tags.map(tag => tag.text.toLowerCase()).includes(tagSearch.toLowerCase())

Limiting the displayed results in React useEffect

I have some React code that is filtering my array and displaying the results by what the user searches:
function App() {
const [countries, setCountries] = useState([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState("");
const [filteredCountries, setFilteredCountries] = useState([]);
useEffect(() => {
setLoading(true);
axios
.get("https://www.countriesapi.com")
.then((res) => {
setCountries(res.data);
setLoading(false);
})
.catch((err) => {
console.log(err);
});
}, []);
useEffect(() => {
setFilteredCountries(
countries.filter((country) =>
country.name.toLowerCase().includes(search.toLowerCase())
)
);
}, [search, countries]);
if (loading) {
return <p>Loading countries...</p>;
}
return (
<div className="App">
<h1>Countries Lists</h1>
<a
style={{ color: "red" }}
target="_blank"
href="https://www.somesite.com"
>
</a>
<input
type="text"
placeholder="Search Countries"
onChange={(e) => setSearch(e.target.value)}
/>
{filteredCountries.map((country, idx) => (
<CountryDetail key={idx} {...country} />
))}
</div>
);
}
My countries list has 500 results so having them all in the page before the user writes the whole search term is not feasible.
How can I display let's say only 10 results in the page, rather than the whole 500?
Thanks
Note: My code has been mirrored by this one if you want to take a look at a live example
You could use Array.prototype.slice() to get only 10 elements.
{filteredCountries.slice(0, 10).map((country, idx) => (
<CountryDetail key={idx} {...country} />
))}
You can paginate your list on axios.get("https://www.countriesapi.com") if www.countriesapi.com allows this. Or you can just make countries.slice
useEffect(() => {
const newList = search ? countries : countries.slice(0, 10)
setFilteredCountries(
newList.filter((country) =>
country.name.toLowerCase().includes(search.toLowerCase())
)
);
}, [search, countries]);

React: prevent variables from re-rendering

i recently found the issue of my problem but it's not solved yet, so i have a variable named data that have a bunch array of objects, i want to move it to global variable so it's not going to re-rendering but i can't because the fetchedData state prevent me from doing this and i can't move my state to global variable,i've tried several method like making a function outside it's component or using react.memo but it solve nothing. is there a way to achieve this?
here's my code Table.js :
import DataTable from "react-data-table-component";
import Columns from "./Columns.js";
...
const Table = () => {
let [page, setPage] = useState(1);
const [fetchedData, setFetchedData] = useState([]);
var [selectedRowsData, setSelectedRowsData] = useState();
const classes = useStyles();
const dispatch = useDispatch();
const checkBoxProps = {
color: "primary",
};
useEffect(() => {
const fetchAPI = async () => {
setFetchedData(
await dispatch(
fetchTableList(
localStorage.getItem("userId"),
localStorage.getItem("auth"),
page
)
)
);
};
fetchAPI();
}, [page]);
const data = [
...fetchedData.map((val, i) => ({
no: val.ROW_NUMBER,
customerId: val.CUSTOMER_ID,
customerName: val.CUSTOMER_NAME,
poDate: val.PO_DATE,
branchId: val.BRANCH_ID,
passangerId: val.PASSANGER_ID,
passangerBank: val.PASSANGER_BANK_NAME,
passangerBankBranch: val.PASSANGER_BANK_BRANCH,
created: val.CREATEBY,
})),
];
const handleChange = (state) => {
console.log(state.selectedRows);
};
return (
<div>
<TableHeader />
<div>
<div>
<DataTable
columns={Columns}
data={data}
selectableRows
selectableRowsComponent={Checkbox}
selectableRowsComponentProps={checkBoxProps}
onSelectedRowsChange={handleChange}
noDataComponent={"Fetching data, please wait..."}
selectableRowsHighlight
customStyles={CustomTableStyle}
conditionalRowStyles={CustomRowBackColor}
dense
/>
<div className={classes.footer}>
<Footer />
<div className={classes.paginationBox}>
<Pagination
nextPage={nextPage}
previousPage={previousPage}
page={page}
setPage={setPage}
/>
</div>
</div>
</div>
</div>
</div>
);
};
export default Table;```
useMemo can be used to control when a given variable inside a functional component is recomputed; e.g.
const data = useMemo(
() => fetchedData.map(val => ({
no: val.ROW_NUMBER,
customerId: val.CUSTOMER_ID,
customerName: val.CUSTOMER_NAME,
poDate: val.PO_DATE,
branchId: val.BRANCH_ID,
passangerId: val.PASSANGER_ID,
passangerBank: val.PASSANGER_BANK_NAME,
passangerBankBranch: val.PASSANGER_BANK_BRANCH,
created: val.CREATEBY,
})),
[fetchedData],
)

Recommendation on how to update an array if the index changes

I want to update array elements and I´m using the index to reference the position. the problem is that the index change while searching for names (keyword) it basically sets the name to the wrong element in the users array (because is taking the indes from the filtered users array)
const [users, setUsers] = useState(["John", "Marty", "Mary", "Johanna"]);
const [keyword, setKeyword] = useState("")
const updateName = (index, name) => {
const newState = [...users];
newState[index] = name;
setNames(newState);
};
I have an input field to search for names
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="search"/>
and then I render a child component with each name and I pass a prop to update the name
users
.filter((user) =>
user.toLowerCase().includes(keyword.toLowerCase())
)
.map((user, index) => (
<User
user={user}
updateName={updateName}
index={index}
key={index}
/>
))
My User component
const User (props) => <button onClick={props.updateName(index, "some name")}>{props.user}</button>
this works perfectly fine. except when keyword changes. because the users.map will change and obviously will change the index. but the problem is that I´m using the index to update the array elements.
for example if I search for the "Ma" keyword. 2 names match so the index of the filtered users will change to 0, 1 but the users array is still the same.
How can I solve this problem? thank you.
If you want to keep your current data structure, you could just forgo the filter and do the filtering within your map function by only conditionally rendering the User component. This way, you don't lose accounting of your indexes.
users
.map((user, index) => (
user.toLowerCase().includes(keyword.toLowerCase()) && <User
user={user}
updateName={updateName}
index={index}
key={index}
/>
))
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const names = ["John", "Marty", "Mary", "Johnna"];
// create a map of user objs to hold the position and name of each user
const mappedUsers = names.map((name, i) => {
return { position: i, name };
});
const [users, setUsers] = useState(mappedUsers);
const [keyword, setKeyword] = useState("");
const updateName = (position, name) => {
setUsers(prevUsers => {
prevUsers[position].name = name;
// return a new merged array of users
return [...prevUsers];
});
};
const User = ({ updateName, user }) => (
<div>
<button onClick={() => updateName(user.position, "someName")}>
{user.name}
</button>
</div>
);
const UserList = ({ users, keyword }) => {
return users
.filter(user => user.name.toLowerCase().includes(keyword.toLowerCase()))
.map(user => (
<User key={user.position} updateName={updateName} user={user} />
));
};
return (
<>
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="search"
/>
<UserList users={users} keyword={keyword} />
</>
);
}
I suggest you stop using index for keeping track of positions of items in an array as this will most likely cause issues when items get deleted or added. Instead, you should create a unique identifier for each item and keep track of them that way.
Here's an example of one way you could go about this:
function MyComponent() {
const initialUsers = ["John", "Marty", "Mary", "Johanna"].map(
(name, idx) => {
return { id: idx + 1, name };
}
);
const [users, setUsers] = React.useState(initialUsers);
const [keyword, setKeyword] = React.useState("");
const handleOnChange = event => {
const { value } = event.target;
const nextUsers = initialUsers.filter(user =>
user.name.toLowerCase().includes(value)
);
setUsers(nextUsers);
setKeyword(value);
};
return (
<div>
<p>Search for a name:</p>
<input
type="text"
onChange={handleOnChange}
value={keyword}
placeholder="Type..."
/>
<ul>
{users.map(user => (
<li>
[{user.id}] {user.name}
</li>
))}
</ul>
</div>
);
}
Here's a working example so that you can test it:
CodeSandbox
Hope this helps.

Categories

Resources