I am trying to filter an array of objects based on both title and description.
It is working fine for a single item, how can I filter items based on both title and description.
Here is my code.
import * as React from "react"
const items = [
{
title: "React",
description:
"React (also known as React.js or ReactJS) is a JavaScript library for building user interfaces. It is maintained by Facebook and a community of individual developers and companies."
},
];
export default function App () {
const [searchTerm, setSearchTerm] = React.useState("");
const inputRef = React.useRef();
React.useEffect(() => {
inputRef.current.focus();
}, []);
const handleChange = event => {
setSearchTerm(event.target.value);
};
const filteredNames = items
.filter((entry) =>
entry.title.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<>
<header>
<input
type="text"
placeholder="Type to filter..."
value={searchTerm}
onChange={handleChange}
ref={inputRef}
/>
</header>
<ul>
{filteredNames.map(({title, description}, i) => (
<li key={i} className="box">
<h1>{title}</h1>
<p>{description}</p>
</li>
))}
</ul>
</>
)
}
I tried with filtered chaining
const filteredNames = items
.filter((entry) =>
entry.title.toLowerCase().includes(searchTerm.toLowerCase())
).filter((entry) =>
entry.description.toLowerCase().includes(searchTerm.toLowerCase())
)
However, this not working.
Your approach is the same as using an AND conditional. The first filter returns only title matches and the second returns title matches that also have description matches. You want OR instead
You can use Array#some() as an OR conditional
const filteredNames = items.filter(({title,description}) =>
const term = searchTerm.toLowerCase();
return [title, description].some(str => str.toLowerCase().includes(term));
})
Filter function expects a boolean query, and will run through the iterable list, and return every item that will return true for that boolean query.
const filteredNames = items.filter(item => {
return item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.description.toLowerCase().includes(searchTerm.toLowerCase())
})
Related
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.
I'm having trouble deleting elements. Instead of deleting a specific element, it only deletes the last newly created element. I'm not sure where I'm going wrong here. I referenced this tutorial that shows what I kinda want to do. (I'm new to React)
import React,{useState, useRef} from "react";
const Body = () => {
const [list, setList] = useState([]);
const AddInput = () => {
setList([...list, {placeholder:"Class Name"}]);
};
const DeleteInput = (index) => {
const l = [...list];
l.splice(index,1);
setList(l);
};
const InputChangeHandler = (event, index) => {
const l = [...list];
(l[index]).value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key)=>
<div key={key}>
<input type={"text"} id={key} placeholder={item.placeholder} onChange={e=>InputChangeHandler(e, key)}/>
<button id={key} onClick={() => DeleteInput(key)}>Delete</button>
</div>
)}
</div>
);
}
export default Body;
Element (input fields + button):
Deletes Last Created:
I think the main problem is the key of items that you set as react doc says:
When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a
key. If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
As in this Article says:
Reordering a list, or adding and removing items from a list can cause issues with the component state, when indexes are used as keys. If the key is an index, reordering an item changes it. Hence, the component state can get mixed up and may use the old key for a different component instance.
What are some exceptions where it is safe to use index as key?
-If your list is static and will not change.
-The list will never be re-ordered.
-The list will not be filtered (adding/removing items from the list).
-There are no ids for the items in the list.
If you set an reliable key in your items with some counter or id generator your problem would solve.
something like this:
export default function App() {
const [list, setList] = useState([]);
const id = useRef({ counter: 0 });
const AddInput = () => {
console.log(id);
setList([...list, { placeholder: "Class Name", id: id.current.counter++ }]);
};
const DeleteInput = (id) => {
setList(list.filter((item, i) => item.id !== id));
};
const InputChangeHandler = (event, index) => {
const l = [...list];
l[index].value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key) => (
<div key={item.id}>
<input
type={"text"}
id={key}
placeholder={item.placeholder}
onChange={(e) => InputChangeHandler(e, key)}
/>
<button id={item.id} onClick={() => DeleteInput(item.id)}>
Delete
</button>
</div>
))}
</div>
);
}
Use filter.
const DeleteInput = (index) => {
const l = list.filter((_, i) => i !== index);
setList(l);
};
Pass id to your DeleteInput function and for remove just filter the item list with id
const DeleteInput = (id) => {const filterItemList = list.filter((item) => item.id!== id);setList(filterItemList ); };
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>
);
}
I have a list of items that I would like the user to be able to sort and filter by attribute. I figure using selectors is the most efficient way to do this, though I am not sure exactly how? Ideally, I would like the user to select an attribute from a dropdown above the list, then enter the actual value of the attribute into a text field which would trigger a filtering selector using both of those params. For example in a list of cars, the user wants to filter by "make" so they choose "make" from a list of other attributes like "make", "model", "year" etc. and then next to that they could type in "Nissan" and then they get a list of just cars made by Nissan.
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchItems } from "../../actions/items";
const ItemList = ({ match }) => {
const items = useSelector((state) => state.items);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchItems());
}, [dispatch]);
const renderedList =
Object.values(items).length > 0
? Object.values(items)
.map((item) => {
return (
<ListItem key={item.id}>
<ItemCard item={item} handleClick={handleClick} />
</ListItem>
);
})
: null;
return (
<Grid container direction="row" className={classes.cardGrid}>
<Grid item lg={4} className={classes.list}>
<Typography variant="h4" className={classes.title}>
Items
</Typography>
<List dense>{renderedList}</List>
</Grid>
</Grid>
);
};
export default ItemList;
Keep the filter attribute and value in Redux state, and then apply the filtering in a selector.
// selectors.js
const getFilterAttribute = state => state.filterAttribute;
const getFilterValue = state => state.filterValue;
const getFilteredItems = state => {
const filterAttribute = getFilterAttribute(state);
const filterValue = getFilterValue(state);
const items = getItems(state);
if (!filterAttribute || !filterValue) {
return items;
}
// apply your filter the way you need it
return Object.values(items).filter(...)
}
This helps separate the state-selection logic from the presentation logic. Now your component just has to call the selector:
// ItemList.js
const ItemList = (props) => {
const items = useSelector(getFilteredItems);
const renderedList = items.map(item => ...)
return (
...
)
}
EDIT:
The filter component might look like:
const FilterControl = () => {
const dispatch = useDispatch();
const [attribute, setAttribute] = useState('');
const [value, setValue] = useState('');
const onSubmit = () => {
dispatch({ type: 'SET_FILTER', attribute, value });
}
return (
<div>
<label>Attribute</label>
<input value={attribute} onChange={e => setAttribute(e.target.value)} />
<label>Value</label>
<input value={value} onChange={e => setValue(e.target.value)} />
<button onClick={onSubmit}>Filter</button>
</div>
)
}
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.