I'm new in React (and in Javascript in general...) and I struggle to manage filters correctly. I think the problem comes from the fact that the calls are asynchronous but I cannot figured it out.
I have an application where you have matches and pronostics. You can filter matchs with a search box, as well as with a calendar:
If I click on a day on the calendar, the list of match must be filtered, but not the calendar itself, otherwise I will not be able to click on any other day
If I use the searchbox, both the matchs and the calendar must be updated
Both are OK in the fact that the matches are correctly filtered according to the day and/or search box, but it is not refreshed correctly, and the values of the pronostics and the score are completely messy after filtering
I have these 3 functions:
handleSelectDay = (selectedDate) => {
this.setState({selectedDate},this.handleFilters)
}
handleSearch = (search) => {
this.setState({search},this.handleFilters)
}
handleFilters = () => {
console.log(this.state.selectedDate)
const filteredMatchsWithoutDate = this.state.matchs
.filter(match => {
const valArray = this.state.search.toLowerCase().split(' ');
let matchBool = true;
for(let i = 0; i < valArray.length; i++) {
if (
match.team_a.name.toLowerCase().includes(valArray[i])
|| match.team_b.name.toLowerCase().includes(valArray[i])
|| match.location.toLowerCase().includes(valArray[i])
|| match.type.toLowerCase().includes(valArray[i])
){
matchBool = true;
}else{
matchBool = false;
}
}
return matchBool
});
const filteredMatchs = filteredMatchsWithoutDate
.filter(match => {
if(this.state.selectedDate != null)
return isSameDay(parseISO(match.schedule),this.state.selectedDate)
return true
})
}
and my render function:
render() {
return (
<div className="App">
<main>
<Navigation />
<Container>
<Filters
onSearch={this.handleSearch}
onCheckbox={this.handleCheckbox}
/>
<Row>
<Col sm={0} md={0} lg={4} xl={4} className="col-xxl-3">
<Calendar
onSelectDay={this.handleSelectDay}
matchs={this.state.filteredMatchsWithoutDate}
/>
</Col>
<Col sm={12} md={12} lg={8} xl={8} className="col-xxl-9">
<Row>
{ this.state.filteredMatchs.map(match => <Match match={match} />) }
</Row>
</Col>
</Row>
</Container>
</main>
</div>
);
}
I think that the problem comes from this.state.filteredMatchs.map(match => <Match match={match} />) which is not correctly refreshed due to the state filter, but I cannot figure out how I could deal with it and the fact that handleSearch and handleCheckbox are already calling the handleFilters method ahead.
Thanks in advance for your help :)
The fastest fix would be adding a key which you can take from map function like:
this.state.filteredMatchs.map((match, index)=> <Match key={index} match={match} />)
So react knows that it has unique key and knows which one to use.
In real big application you shouldn't use index as a key. Key should be 100% unique so the best is to set it as ID or uuid etc.
It looks like this could be because you are not using key props.
<Match key={someUniqueValue} match={match} />
Without key props react cannot track changes in arrays mapped to elements correctly.
https://reactjs.org/docs/lists-and-keys.html
Edit/ PS. You should never use an array index to map these change and it defeats the purpose of key props - https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318
You need to update your state variable filteredMatchs in your handleFilters method. Your logic is there to filter the data but you are not updating the state. Adding the setState line at the end of your handleFilters method will make it work.
this.setState({filteredMatchs})
Also, as #jazz and #sowam suggested you should also add key attribute to your component.
Related
I have localStorage with multiple items on it. I want to retrieve all of them and display it on a <ListItem> Material UI.
Here's my current code:
function saveJob(key, value) {
localStorage.setItem(key, value);
}
function saveJob is basically just save the value along with unique key.
The content of the localStorage would be something like this:
Key
Value
1
Technician
2
Plumber
How I retrieved the items back:
var savedJobs = [];
useEffect(() => {
var keys = Object.keys(localStorage),
i = keys.length;
while (i--) {
savedJobs.push(localStorage.getItem(keys[i]));
}
return savedJobs;
}, [])
Now onto the problem, I tried to display it in a functional component through <ListItem> Material UI like the following:
<List>
{savedJobs.map((job) => (
<ListItem key={job.Key} disablePadding>
<ListItemButton>
<ListItemText primary={job.Value} />
</ListItemButton>
</ListItem>
))}
</List>
This is not working. Can anyone point me in the right direction?
React doesn't trigger rerender when a local variable in a function component was changed. It will be rerendered when: the state has changed, props were changed or the parent component was rerendered.
You should put your savedJobs to state and write
const [savedJobs, setSavedJobs] = useState([]);
instead of
var savedJobs = [];
You need state to store the data you retrieve from localstorage. Instead of your savedJobs var outside of your useEffect, create an array inside the hook and then update your state with this data. Here's an example:
// Initiate your state as an empty array.
const [savedJobs, setSavedJobs] = useState([]);
// Used as an example instead of localstorage.
const example = ["foo", "bar"];
useEffect(() => {
let jobs = [];
let keys = Object.keys(example),
i = keys.length;
while (i--) {
jobs.push(example[i]);
}
setSavedJobs(jobs);
}, [example]);
return <>{savedJobs.map((job) => job)}</>;
Or see this interactive example.
First I would check what is inside your savedJobs array.
It seems that savedJobs would look like this:
savedJobs = ['Technician', 'Plumber'];
On the other hand, you are mapping and getting the value like so:
{job.Key} & {job.Value}
Try:
<List>
{savedJobs.map((job, i) => (
<ListItem key={i} disablePadding>
<ListItemButton>
<ListItemText primary={job} />
</ListItemButton>
</ListItem>
))}
</List>
ps: you should not use i as a key, but I let you figuring out what value you want there.
If you want to use {job.key} and {job.value}, you need to push an object like so:
savedJobs.push({key: i, value: localStorage.getItem(keys[i])});
I am currently designing an application that contains a list of values in a list, called modifiers, to be edited by the user to then store for later use in calculations. To make it easier to find a specific modifier, I added a search function to the list in order to pull up the similar modifiers together to the user. However, once the user puts in a value into the filtered list and then unfilters the list, the component incorrectly assigns the values to the wrong modifiers. To be more specific, the ant design <List> component when filtered fails to put the proper defaultValue for each associated input. Namely, when I input a value into the first item in the filtered list and then unfilter it, the List incorrectly places that value within the first element on the unfiltered list, rather than the modifier it was supposed to be associated with. It should be putting the proper value with the associated element by assigning the value that its grouped with in the context I have stored, but it obviously fails to do so.
Here is the Ant Design List Component I am talking about, I have removed some callbacks that aren't necessary to understand the problem. The renderitem prop takes the dataSource prop as an input and maps all of the values into it to be inputs for the <List.Item> components.
EDIT:
I failed to mention the hook in the first line, that is utilized by the search function in order to filter the words looked through to update the list accordingly. I also removed some unnecessary inline css and components since they are not relevant to the problem to improve readability. I have also decided to give a more concrete example of my issue:
This is an image of the initial values set by the user.
This is an image immediately after searching the exact name of the modifier and the list gets filtered. Clearly, the value from the first item of the unfiltered list is being put into the input of the first item of the filtered list, which is the main problem. Now when the search is undone, everything does get properly set, so I am unsure how to fix this.
I have some ideas as to why this is occurring. I know that the input components are not being re-rendered, and rather their ids are just being swapped out when the search occurs. So if there are any ways to either forcefully re-render the input components in addition to the list sections, please tell me!
const Modifiers = () => {
const [searchFilter, setSearchFilter] = useState(military); //Only for words in search bar, "military" will be replaced with entire data set later
const context = useContext(Context)
const search = value => {
if(value != ""){
setSearchFilter(searchFilter.filter(mod => mod.name.toLowerCase().indexOf(value.toLowerCase()) != -1))
}
else {
setSearchFilter(military)
}
}
const updateContext = (e, name) => {
let id = name.toLowerCase().replace(/ /gi, "_");
if(context.modifiers[id] != undefined){
context.modifiers[id] = parseFloat(e.target.value)
}
}
return (
<Layout>
<SiteHeader/>
<Content style={{ padding: '1% 3%', backgroundColor: "white"}}>
<Typography>
<Title level={2} style={{textAlign: "center"}}>
Modifier List
</Title>
</Typography>
<List dataSource={searchFilter} header={<div style={{display: "flex"}}> <Title level={3} style={{paddingLeft: "24px"}}>Modifiers</Title> <Button shape="circle" size="large" icon={<InfoCircleOutlined/>}/> <Search allowClear placeholder="Input Modifier Name" enterButton size="large" onSearch={search}
renderItem={mod => (
<List.Item extra={parseTags(mod)}>
<List.Item.Meta description={mod.desc} avatar={<Avatar src={mod.image}/>} title={<div style={{display: "flex"}}><Title level={5}>{mod.name}: </Title> <Input defaultValue={context.modifiers[mod.name.toLowerCase().replace(/ /gi, "_")] != undefined ? context.modifiers[mod.name.toLowerCase().replace(/ /gi, "_")] : ""} id={mod.name} onChange={(e) => updateContext(e, mod.name)}/></div>}/>
</List.Item>
)}
/>
</Content>
</Layout>
);
}
export default Modifiers;
Here is the Context Class, the modifiers field is what is the issue currently. It only has 2 currently, but the problem persists when more are added, and these 2 modifiers are the first in the unfiltered list as well.
export class Provider extends React.Component {
state = {
name: "None Selected",
tag: String,
flag: "images/flags/ULM",
modifiers: {
army_tradition: 0,
army_tradition_decay: 0,
}
}
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}
Here is what one element in the military array looks like for reference as well. The regex inside the <List.Item> component is merely converting the name field of the object into one that matches whats stored within the context.modifiers field.
export const military = [
{
name: "Army Tradition",
desc: "Adds to the rate of army tradition gained each year.",
function: "ADDITIVE",
type: "WHOLE NUMBER",
category: "MILITARY",
image: "/images/icons/landLeaderFire.png",
},
...
Thanks for any help you can give.
I have solved the issue, I replaced the "id" prop with a "key" prop (which the documentation doesn't even tell you about) and now everything works properly!
I have a problem with saving the state of the search query.
When the popover is brought into focus, the searchString starts with undefined (second undefined value in picture). When the key 'b' is pressed, the event is fired, and it sets the value to "" (initialized value). As shown, when "bart" is in the search query, console only registers "bar". Does anyone know why this behavior occurs? The end goal is that I am trying to retain the search string on selection (it disappears onclick) -> would appreciate any help with this. The main code block where these changes are happening:
<Autocomplete
open
onClose={handleClose}
multiple
classes={{
paper: classes.paper,
option: classes.option,
popperDisablePortal: classes.popperDisablePortal,
}}
value={pendingValue}
onChange={(event, newValue) => {
setPendingValue(newValue);
}}
// inputValue={searchString}
// onInputChange={(event, newValue) => {
// setSearchString(newValue);
// }}
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No values"
renderOption={(option, { selected }) => (
<React.Fragment>
<DoneIcon
className={classes.iconSelected}
style={{ visibility: selected ? 'visible' : 'hidden' }}
/>
<div className={classes.text}>
{option.value}
</div>
</React.Fragment>
)}
options={[...suggestions].sort((a, b) => {
// Display the selected labels first.
let ai = selectedValue.indexOf(a);
ai = ai === -1 ? selectedValue.length + suggestions.indexOf(a) : ai;
let bi = selectedValue.indexOf(b);
bi = bi === -1 ? selectedValue.length + suggestions.indexOf(b) : bi;
return ai - bi;
})}
getOptionLabel={option => option.value}
renderInput={params => (
<InputBase
ref={params.InputProps.ref}
inputProps={params.inputProps}
autoFocus
className={classes.inputBase}
// onChange={(event) => {
// console.log("event.target: ", event.target);
// console.log("event.currentTarget: ", event.currentTarget);
// setSearchString(event.currentTarget);
// }}
value={searchString}
onChange={handleInputChange}
/>
)}
/>
I have tried to store the value and re-populate it using both through the Autocomplete props and the InputBase (doing it on both causes it to crash). I have added a sandbox for your ref: CodeSandbox
Appreciate all the help!
Material UI autocomplete by design resets the search value every time you select an option. If you want to by pass it, use useAutocomplete hook to fine tune the component according to your need.
As for delayed console log values, you're setting the new value and then you're console logging the old value. So obviously it will print the old value, what else did you expect?
You code should have been like this
const handleInputChange = event => {
// new value => event.currentTarget.value
// old value => searchString
// these values never mutate throughout this function call
setSearchString(event.currentTarget.value);
// searchString still remains the same here and
// won't change even if you call setState
// it remains the same throughout this entire function call
// Since Mutation is not allowed in Functional Programming
// This is perhaps why Functional Programming is
// far better than Object Oriented Programming 😉
console.log('searchString: ', event.currentTarget.value);
}
However this isn't the right way to observe state changes. Better way would be something like this,
// This will be called whenever React
// observes a change in anyState
useEffect(() => {
console.log(anyState)
}, [anyState])
I am mapping through an array, and I want my variable i to be used as a unique key for my Components, however I do not know how (or where) to increment it correctly, if I add a {i++} within the <Component> tags then it will display the value of i on screen, and if I instead add {this.function(i)} and place the i++ inside the function, it will call the function but the variable i will reinitiate to the value of 0 everytime, so the key value will not be unique. I need the value of i to be the key for the component and it has to be incremented by 1 everytime, does anyone know how I can achieve this? Also, as you can see in the code, when the component is clicked it will make a function call which will send the value of i of the clicked component as a parameter to the called function.
Code:
function(i) {
console.log(i)
}
render() {
var i = 0;
var {array} = this.state;
return (
<div className="App">
{array.map(item => (
<Component key={i} onClick={(e) => this.function(i, e)}>
<p>{item.name}</p>
</Component>
))}
</div>
);
}
The map function gets a second parameter which is the index of the element:
{array.map((item, i) => (
<Component key={i} onClick={(e) => this.function(i, e)}>
<p>{item.name}</p>
</Component>
)}
Be aware that if you intend to sort this array or change its contents at runtime, then using array index as a key can cause some mistakes, as sometimes an old component will be mistake for a new one. If it's just a static array though, then using index as a key shouldn't be a problem.
.map already offer the increment, just add a second variable to the callback
render() {
var {array} = this.state;
return (
<div className="App">
{array.map((item,i) => (
<Component key={i} onClick={(e) => this.function(i, e)}>
<p>{item.name}</p>
</Component>
))}
</div>
);
}
You could try array.map((x, Key) => console.log(key)); ..
In place of console.log you could add your code, it should work fine as per your requirement.
I'm creating a dynamic list component that basically renders a new empty text field every time something is added to the list. The component is as follows (it uses a custom TextFieldGroup but I don't think that code is necessary as nothing out of the ordinary is going on.):
const DynamicInputList = ({ inputs, onChangeArray, label, name, errors }) =>
{
const nonEmptyInputs = inputs.filter(input => {
return input !== "";
});
const lastIndex = nonEmptyInputs.length;
return (
<div>
{nonEmptyInputs.map((input, index) => {
return (
<Grid container spacing={16} key={index}>
<Grid item xs={12}>
<TextFieldGroup
label={label}
id={`${name}${index}`}
name={name}
value={input}
onChange={e => onChangeArray(e, index)}
errors={errors[name]}
/>
</Grid>
</Grid>
);
})}
<Grid container spacing={16} key={lastIndex}>
<Grid item xs={12}>
<TextFieldGroup
label={label}
id={`${name}${lastIndex}`}
name={name}
value={inputs[lastIndex]}
onChange={e => onChangeArray(e, lastIndex)}
errors={errors[name]}
/>
</Grid>
</Grid>
</div>
);
};
The onChangeArray function is as follows:
onChangeArray = (e, index) => {
const state = this.state[e.target.name];
if (e.target.value === "") {
state.splice(index, 1);
this.setState({ [e.target.name]: state });
} else {
state.splice(index, 1, e.target.value);
this.setState({ [e.target.name]: state });
}
};
Everything works fine except when a blank field is changed (begin to type) it immediately removes focus from any of the fields. I'm looking for a way to keep the focus on this field while still adding the new one below.
Thanks in advance.
The problem is that you're encountering the following sequence of events (assuming inputs length of 5):
React renders 1 field (key = 5)
User starts typing 'a'
Your state change is triggered
React renders 2 entirely new fields (key = 1 & value = 'a', key = 5) and trashes the old one (key = 5)
There are at least two solutions
Don't delete / trigger re-render of the original field
This is the cleaner solution.
First, you are directly mutating state in onChangeArray with your use of slice, because that does an in-place modification. Perhaps this isn't causing problems now, but it's a big React anti-pattern and could create unpredictable behavior.
Here's a quick fix:
onChangeArray = (e, index) => {
const state = [...this.state[e.target.name]];
if (e.target.value === "") {
state.splice(index, 1);
this.setState({ [e.target.name]: state });
} else {
state.splice(index, 1, e.target.value);
this.setState({ [e.target.name]: state });
}
};
And other options
I'll leave the reworking of the implementation to you, but the gist would be:
Use consistent key references, which means iterate over all your inputs and just skip the empty ones instead of finding the empty ones first and setting that arbitrary key to the total length. Of course, you should still render the first empty one.
You could consider rendering everything, and use CSS display:none attribute as needed to accomplish your desired UX
Leverage input's autofocus attribute
I assume your TextFieldGroup uses an input element. You could pass the autofocus attribute to the field you want to have focus. This answers your original question.
But this isn't recommended, as you're still needlessly running through those re-render cycles and then putting a patch on top.