I'm pretty new to using hooks and functional components.
I have a Filtered List. When I try to update the filter, it will use the last filter state instead of the new one. I must be missing some render/state change orders, but I can't seem to figure out what it is.
I appreciate any help I can get :)
Pseudo code below:
export default function TransferList(props) {
const [wholeList, setWholeList] = React.useState([]);
const [filteredList, setFilteredList] = React.useState([]);
const [filter, setFilter] = React.useState([]);
return (
<>
<TextField
value={filter}
onChange={(e) => {
// Set filter input
setFilter(e.target.value);
// Filter the list
let filtered = wholeList.filter(
(item) => item.indexOf(filter) !== -1
);
setFilteredList(filtered);
}}
/>
<List>
{filteredList.map((item) => (
<ListItem>Item: {item}</ListItem>
))}
</List>
</>
);
}
Inside onChange you should use save the value in a constant, filter will not update just after setFilter(filterValue) as this is an async operation.
<TextField
value={filter}
onChange={e => {
const filterValue = e.target.value;
// Set filter input
setFilter(filterValue);
// Filter the list
let filtered = wholeList.filter(item => item.indexOf(filterValue) !== -1);
setFilteredList(filtered);
}}
/>;
State updates are asynchronous and hence the filter state update doesn't reflect immediately afterwords
You must store the new filter values and set the states based on that
export default function TransferList(props) {
const [wholeList, setWholeList] = React.useState([]);
const [filteredList, setFilteredList] = React.useState([]);
const [filter, setFilter] = React.useState([]);
return (
<>
<TextField value={filter} onChange={(e) => {
// Set filter input
const newFilter = e.target.value;
setFilter(newFilter)
// Filter the list
let filtered = wholeList.filter(item => item.indexOf(newFilter) !== -1)
setFilteredList(filtered)
}} />
<List>
{filteredList.map(item => <ListItem>Item: {item}</ListItem>)}
</List>
</>
)
}
So it turns out I had to give the initial filtered list the entire unfiltered list first. For some reason that fixes it.
const [filteredList, setFilteredList] = React.useState(props.wholeList);
I initially wanted an empty filter to display nothing, but I may have to settle for showing the entire list when the filter is empty
Order your code to be increase readability
In clean code
Main changes:
Use destructuring instead of props
Out the jsx from the html return to increase readability
Use includes instead of indexOf
Add key to the list
export default function TransferList({ wholeList }) {
const [filteredList, setFilteredList] = React.useState(wholeList);
const [filter, setFilter] = React.useState([]);
const handleOnChange = ({ target }) => {
setFilter(target.value);
const updatedFilteredList = wholeList.filter(item => item.includes(target.value));
setFilteredList(updatedFilteredList);
}
const list = filteredList.map(item => {
return <ListItem key={item}>Item: {item}</ListItem>
});
return (
<>
<TextField value={filter} onChange={handleOnChange} />
<List>{list}</List>
</>
);
}
and I have a filter component the do the filter list inside.
import React, { useState } from 'react';
function FilterInput({ list = [], filterKeys = [], placeholder = 'Search',
onFilter }) {
const [filterValue, setFilterValue] = useState('');
const updateFilterValue = ev => {
setFilterValue(ev.target.value);
const value = ev.target.value.toLowerCase();
if (!value) onFilter(list);
else {
const filteredList = list.filter(item => filterKeys.some(key => item[key].toLowerCase().includes(value)));
onFilter(filteredList);
}
}
return (
<input className="filter-input" type="text" placeholder={placeholder}
value={filterValue} onChange={updateFilterValue} />
);
}
export default FilterInput;
the call from the father component look like this
<FilterInput list={countries} filterKeys={['name']} placeholder="Search Country"
onFilter={filterCountries} />
this is in my corona app.. you can look on my Github.
and see how I build the filter
(https://github.com/omergal99/hello-corona)
Related
I was watching a tutorial on how to make todos, though my main focus was local storage use.
But when he made the delete button then I was a bit confused, the code below shows how he did it but I am not getting it.
Can anyone explain that I tried using the splice method to remove items from the array but I am not able to remove the items from the page?
Can you also suggest what should I do after using splice to return the array on the page?
Below is the code,
import "./styles.css";
import { useState, useEffect } from 'react'
import Todoform from './TodoForm'
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (e) => {
// TODO: items.splice(e-1, 1);
// Is there any other way I can do the below thing .i.e
// to remove todos from page.
// this is from tutorial
setitems((e1)=>{
return e1.filter((er , index)=>{
return index!=e-1;
})
})
}
return (
<>
<div className='display_info'>
<h1>TODO LIST</h1>
<br />
<input onChange={itemevent} value={list} type="text" name="" id="" />
<br />
<button onClick={listofitem} >Add </button>
<ul>
{
items.map((e, index) => {
index++;
return (
<>
<Todoform onSelect={deleteItems} id={index} key={index} index={index} text={e} />
</>
)
})
}
</ul>
</div>
</>
)
}
And this is the TodoForm in this code above,
import React from 'react'
export default function Todoform(props) {
const { text, index } = props;
return (
<>
<div key={index} >
{index}. {text}
<button onClick={() => {
props.onSelect(index)
}} className="delete">remove</button>
</div>
</>
)
}
Here is the codeSandbox link
https://codesandbox.io/s/old-wood-cbnq86?file=/src/TodoForm.jsx:0-317
I think one issue with your code example is that you don't delete the todo entry from localStorage but only from the components state.
You might wanna keep localStorage in sync with the components state by using Reacts useEffect hook (React Docs) and use Array.splice in order to remove certain array elements by their index (Array.splice docs).
// ..
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
/* As this `useEffect` has an empty dependency array (the 2nd parameter), it gets called only once (after first render).
It initially retrieves the data from localStorage and pushes it to the `todos` state. */
useEffect(() => {
const todos = JSON.parse(localStorage.getItem("notes"));
setitems(todos);
}, [])
/* This `useEffect` depends on the `items` state. That means whenever `items` change, this hook gets re-run.
In here, we set sync localStorage to the current `notes` state. */
useEffect(() => {
localStorage.setItem("notes", JSON.stringify(items));
}, [items])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (index) => {
// This removes one (2nd parameter) element(s) from array `items` on index `index`
const newItems = items.splice(index, 1)
setitems(newItems)
}
return (
<>
{/* ... */}
</>
)
}
There are multiple ways to remove an item from a list in JS, your version of splicing the last index is correct too and it is able to remove the last item. What it can't do is update your state.
His code is doing two things at the same time: Removing the last item of the Todo array and then, setting the resulted array in the state which updates the todo list.
So, change your code as
const deleteItems = (e) => {
let newItems = [...items];
newItems.splice(e-1, 1);
setitems(newItems);
}
I want to add and remove the components dynamically, so far just i can add, but when i tried to remove it remove too weird, lets say i dont want to remove just i would like to hide the components
import {
MinusCircleOutlined,
PlusOutlined,
} from '#ant-design/icons'
import { useState } from "react"
const MyInput = ({ index, removeInput }) => {
return (<div >
<Input placeholder="Email address" />
<MinusCircleOutlined className="icon-left" onClick={() => { removeInput(index) }} />
</div>
)
}
const MyComponent = ({ }) => {
const [form] = Form.useForm()
const [index, setIndex] = useState(0)
const [inputsFields, setInputsFields] = useState([])
const [hiddenFields, setHiddenFields] = useState([])
const AddInput = () => {
const newInviteField = <MyInput index={index} removeInput={removeInput} />
setInputsFields([...inputsFields, newInviteField])
const newIndex = index + 1
setIndex(newIndex)
}
const removeInput = (currentIndex) => {
let a = hiddenFields
a.push(currentIndex)
setHiddenFields([...a])
}
return (
<Card>
<Form form={form} layout="vertical">
<Form.Item className='form-item item-container'>
{inputsFields.map((item, index) => !hiddenFields.includes(index) && <div key={index}>{item}</div>)}
</Form.Item>
<Form.Item >
<a href="#" onClick={AddInput}>Add</a>
</Form.Item>
</Form>
</Card>)
}
i tried to filter by the index, just showing the indexes does not into the hidden array !hiddenFields.includes(index)
the problem is when i am deleting, sometimes it is not deleting, sometimes other component is deleting
You should never use an array method index as key, if you modify the array. It has to be unique. When you delete element with index 2, the index 3 becomes index 2. This should not happend. You should not change the key prop value
The solution:
keep information about the inputs in the state, not the inputs itself.
// keep necessarry information for each input here.
// Like id, name, hidden, maybe what to render. Whatever you want
const [inputsFields, setInputsFields] = useState([{
id: 'name',
hidden: false
}])
// and map them
inputsFields.map(element => !element.hidden && <Input key={element.id} />)
When each element has unique id, you will delete the element with that id, not with the array map index
If you do not need that much info. Just make array of numbers in that state,
const counter = useRef(1)
const [inputsFields, setInputsFields] = []
const AddInput = () => {
counter.current += 1
setInputsFields(oldInputs => [...oldInputs, counter.current])
}
// and render them:
inputsFields.map(element => <Input key={element} />)
I am making a front-end UI returning student objects (grades, email, etc.) from an API call. I currently have a filter set up to return objects by names. I need to set up a second filter by tags, which can be added through an input element within each student component returned from .map() the API. I cannot figure out how to set up the filter as the tags are stored within each instance of the student Profile.js component. Can you please help me? Ultimately the UI should return search results from both filters (name && tags)
Snippet from App.js:
function App() {
const [students, setStudents] = useState([])
const [filteredStudents, setFilteredStudents] = useState([])
const [search, setSearch] = useState("")
// Get request and store response in the 'students' state //
useEffect(()=>{
axios.get('Link to the API')
.then(res => {
const result = res.data.students
setStudents(result)
})
},[])
// Filter students by name and store filtered result in 'filteredStudents' //
useEffect(() => {
const searchResult = []
students.map(student => {
const firstName = student.firstName.toLowerCase()
const lastName = student.lastName.toLowerCase()
const fullName = `${firstName}` + ` ${lastName}`
if (fullName.includes(search.toLowerCase())) {
searchResult.push(student)
}
return
})
setFilteredStudents(searchResult)
}, [search])
return (
<div>
<SearchBar
search={search}
onChange={e => setSearch(e.target.value)}
/>
//Second search bar by tag here//
{search.length == 0 &&
//unfiltered students object here
}
{search.length != 0 &&
<div>
{filteredStudents.map(student => (
<Profile
//Some props here//
/>
))}
</div>
}
</div>
)}
Snippet from Profile.js
//bunch of code before this line//
const [tags, setTags] = useState([])
const [tag, setTag] = useState("")
function handleKeyPress(e) {
if(e.key === 'Enter') {
tags.push(tag)
setTag("")
}
}
return(
<div>
//bunch of code before this line//
<Tag
onChange={e => setTag(e.target.value)}
onKeyPress={handleKeyPress}
tags={tags}
tag={tag}
/>
</div>
)
Snippet from Tag.js:
export default function Tag({tag, tags, onChange, onKeyPress}) {
return (
<div>
{tags.length > 0 &&
<div>
{tags.map(tag => (
<span>{tag}</span>
))}
</div>
}
<input
type='text'
value={tag}
placeholder="Add a tag"
key='tag-input'
onKeyPress={onKeyPress}
onChange={onChange}
/>
</div>
)
}
Edit
With your comment I think I now understand what you're trying to do, the images you provided really helped. You want to change the student object whenever a tag is added in the Profile component if I'm not mistaken (again, correct me if I am). That would mean the Profile component needs access to a handler so that whenever a tag is added, it also sets a new students state. It would look like this:
App.js
function App() {
const [students, setStudents] = useState([]);
const [filteredStudents, setFilteredStudents] = useState([]);
const [search, setSearch] = useState("");
const handleTagAdded = (tag, index) => {
setStudents((prevStudents) => {
// We copy object here as the student we're accessing
// is an object, and objects are always stored by reference.
// If we didn't do this, we would be directly mutating
// the student at the index, which is bad practice
const changedStudent = {...prevStudents[index]};
// Check if student has 'tags` and add it if it doesn't.
if (!("tags" in changedStudent)){
changedStudent.tags = [];
}
// Add new tag to array
changedStudent.tags.push(tag);
// Copy array so we can change it
const mutatableStudents = [...prevStudents];
mutatableStudents[index] = changedStudent;
// The state will be set to this array with the student
// at the index we were given changed
return mutatableStudents;
})
}
// Get request and store response in the 'students' state //
useEffect(() => {
axios.get("Link to the API").then((res) => {
const result = res.data.students;
setStudents(result);
});
}, []);
// Filter students by name and tag, then store filtered result in //'filteredStudents'
useEffect(() => {
// Array.filter() is perfect for this situation //
const filteredStudentsByNameAndTag = students.filter((student) => {
const firstName = student.firstName.toLowerCase();
const lastName = student.lastName.toLowerCase();
const fullName = firstName + lastName;
if ("tags" in student){
// You can now do whatever filtering you need to do based on tags
...
}
return fullName.includes(search.toLowerCase()) && yourTagComparison;
});
setFilteredStudents(filteredStudentsByNameAndTag);
}, [search]);
return (
<div>
<SearchBar search={search} onChange={(e) => setSearch(e.target.value)} />
//Second search bar by tag here //
{search.length === 0 &&
// unfiltered students //
}
{search.length !== 0 && (
<div>
{filteredStudents.map((student, index) => (
<Profile
// Some props here //
onTagAdded={handleTagAdded}
// We give the index so Profile adds to the right student
studentIndex={index}
/>
))}
</div>
)}
</div>
);
}
In handleTagAdded, I copy the object at prevStudents[index] because it is a reference. This may sound odd if you don't know what I'm referring to (pun intended). Here is a link to an article explaining it better than I will be able to.
Profile.js
function Profile({ onTagAdded, studentIndex }) {
// Other stuff //
const [tags, setTags] = useState([]);
const [tag, setTag] = useState("");
const handleTagKeyPress = (e) => {
if (e.key === "Enter") {
// Use this instead of tags.push, when changing state you always
// must use the `setState()` function. If the new value depends on the
// previous value, you can pass it a function which gets the
// previous value as an argument like below. It is also bad
// practice to change, or 'mutate' the argument you're given
// so we instead copy it and change that.
setTags((previousTags) => [...previousTags].push(tag));
setTag("");
onTagAdded(tag, studentIndex)
}
};
return (
<div>
// Other stuff
<Tag onChange={(e) => setTag(e.target.value)} onKeyPress={handleTagKeyPress} tags={tags} tag={tag} />
</div>
);
}
Now, each <Profile /> component has its own tags state, but through the use of handleTagAdded(), we can change the student within each profile component based on tags.
Apologies for the confusion in my first answer, I hope this solves your issue!
Old answer
There's a very important concept in React known as "Lifting State". What this means is that if a parent component needs to access the state of a child component, one solution is to 'lift' the state from the child to the parent.
You can read some more about it in the React documentation.
In this example, you need to lift the tag state up from the <Profile /> component to the <App /> component. That way, both search and tag are in the same place and can be compared.
I believe the code below is along the lines of what you want:
App.js
function App() {
const [students, setStudents] = useState([]);
const [filteredStudents, setFilteredStudents] = useState([]);
const [tags, setTags] = useState([]);
const [tag, setTag] = useState("");
const [search, setSearch] = useState("");
const handleTagChange = (e) => setTag(e.target.value);
const handleTagKeyPress = (e) => {
if (e.key === "Enter") {
// Use this instead of tags.push, when changing state you always
// must use the `setState()` function. If the new value depends on the
// previous value, you can pass it a function which gets the
// previous value as an argument like below.
setTags((previousTags) => previousTags.push(tag));
setTag("");
}
};
// Get request and store response in the 'students' state //
useEffect(() => {
axios.get("Link to the API").then((res) => {
const result = res.data.students;
setStudents(result);
});
}, []);
// Filter students by name and tag, then store filtered result in //'filteredStudents'
useEffect(() => {
// Array.filter() is perfect for this situation //
const filteredStudentsByNameAndTag = students.filter((student) => {
const firstName = student.firstName.toLowerCase();
const lastName = student.lastName.toLowerCase();
const fullName = firstName + lastName;
return fullName.includes(search.toLowerCase()) && student.tag === tag;
});
setFilteredStudents(filteredStudentsByNameAndTag);
}, [search]);
return (
<div>
<SearchBar search={search} onChange={(e) => setSearch(e.target.value)} />
//Second search bar by tag here //
{search.length == 0 &&
// unfiltered students //
}
{search.length != 0 && (
<div>
{filteredStudents.map((student) => (
<Profile
// Some props here //
onChange={handleTagChange}
onKeyPress={handleTagKeyPress}
tag={tag}
tags={tags}
/>
))}
</div>
)}
</div>
);
}
Profile.js
function Profile({ onChange, onKeyPress, tags, tag }) {
// Other stuff //
return (
<div>
// Other stuff
<Tag onChange={onChange} onKeyPress={onKeyPress} tags={tags} tag={tag} />
</div>
);
}
We've moved the tag state up to the <App /> component, so now when we filter we can use both the search and tag. I also changed students.map to students.filter as it is a better alternative for filtering an array.
I'm not clear on how you wanted to filter the tags, so I assumed the student object would have a tag attribute. Feel free to correct me about how the data is structured and I'll reformat it.
I hope this helped, let me know if you have any more problems.
I need to use an input filter in React. I have a list of activities and need to filter them like filters on the picture. If the icons are unchecked, actions with these types of activities should not be showed. It works.
The problem that when I use input filter and write letters it works. But when I delete letter by letter nothing changes.
I understand that the problem is that I write the result in the state. And the state is changed.But how to rewrite it correctly.
const [activities, setActivities] = useState(allActivities);
const [value, setValue] = useState(1);
const [checked, setChecked] = useState<string[]>([]);
const switchType = (event: React.ChangeEvent<HTMLInputElement>)=> {
const currentIndex = checked.indexOf(event.target.value);
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(event.target.value);
} else {
newChecked.splice(currentIndex, 1);
}
//function that shows activities if they are checked or unchecked
setChecked(newChecked);
const res = allActivities.filter(({ type }) => !newChecked.includes(type));
setActivities(res);
};
//shows input filter
const inputSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
const foundItems = activities.filter(
(item) => item.activity.indexOf(event.target.value) > -1
);
setActivities(foundItems);
};
//shows participants filter
const countSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(Number(event.target.value));
const participantsSearch = allActivities.filter(
(item) => item.participants >= event.target.value
);
setActivities(participantsSearch);
};
This is render part
<Input
onChange={inputSearch}
startAdornment={
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
}
/>
<Input
onChange={countSearch}
type="number"
value={props.value}
startAdornment={
<InputAdornment
position="start"
className={classes.participantsTextField}
>
<PersonIcon />
</InputAdornment>
}
/>
The issue here is that you save over the state that you are filtering, so each time a filter is applied the data can only decrease in size or remain the same. It can never reset back to the full, unfiltered data.
Also with the way you've written the checkbox and input callbacks you can't easily mix the two.
Since the filtered data is essentially "derived" state from the allActivities prop, and the value and checked state, it really shouldn't also be stored in state. You can filter allActivities inline when rendering.
const [value, setValue] = useState<sting>('');
const [checked, setChecked] = useState<string[]>([]);
const switchType = (event: React.ChangeEvent<HTMLInputElement>)=> {
const currentIndex = checked.indexOf(event.target.value);
if (currentIndex === -1) {
setChecked(checked => [...checked, event.target.value]);
} else {
setChecked(checked => checked.filter((el, i) => i !== currentIndex);
}
};
const inputSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value.toLowerCase());
};
...
return (
...
{allActivites.filter(({ activity, type }) => {
if (activity || type) {
if (type) {
return checked.includes(type);
}
if (activity) {
return activity.toLowerCase().includes(value);
}
}
return true; // return all
})
.map(.....
The problem lies with your inputSearch code.
Each time you do setActivities(foundItems); you narrow down the state of your activities list. So when you start deleting, you don't see any change because you removed the rest of the activities from the state.
You'll want to take out allActivities into a const, and always filter allActivities in inputSearch, like so:
const allActivities = ['aero', 'aeroba', 'aerona', 'aeronau'];
const [activities, setActivities] = useState(allActivities);
// ...rest of your code
const inputSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
const foundItems = allActivities.filter(
(item) => item.activity.indexOf(event.target.value) > -1
);
setActivities(foundItems);
};
// ...rest of your code
I am attempting to use react-bootstrap-typeahead as the search input for my react-data-table-component so that there would be an autosuggest option. My problem is on how could i reflect the selected option to my dataTable in such it would only show the specific item chosen (or paginate). Another problem is that I wasn't able to reflect the filteredItems as the option of the typeahead
const [filterText, setFilterText] = React.useState('');
const [resetPaginationToggle, setResetPaginationToggle] = React.useState(
false
);
//Data is passed to filtered items to sort
const filteredItems =
projects &&
projects.filter(
(project) =>
project.name.toString().toLowerCase().includes(filterText) ||
project?.description.toString().toLowerCase().includes(filterText)
);
const [singleSelections, setSingleSelections] = useState([]);
const subHeaderComponentMemo = React.useMemo(() => {
const handleClear = () => {
if (filterText) {
setResetPaginationToggle(!resetPaginationToggle);
setFilterText('');
}
};
let singleSelections = filterText;
return (
<Typeahead
labelKey='name'
onChange={(e) => setSingleSelections(e.target.value)}
onClear={handleClear}
options={filteredItems}
placeholder='Search'
selected={singleSelections}
/>
);
}, [filterText, resetPaginationToggle]);
This is my DataTable:
<DataTable
defaultSortAsc='id'
pagination
paginationResetDefaultPage={resetPaginationToggle}
columns={columns}
subHeader
subHeaderComponent={subHeaderComponentMemo}
persistTableHead
expandableRows
expandOnRowClicked
expandableRowsComponent={<ExpandedComponent data={filteredItems} />}
data={filteredItems}
/>