Autocomplete in Popover Component with search rentention - javascript

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])

Related

React Component List fails to properly update the default value of Input components when Filtered

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!

Why id and itemId never match in that react code?

This is a simple representation of my real problem in a project, it seems i am missing something in react or may be the problem is not related to react, this is my code https://codesandbox.io/s/lucid-northcutt-c3fw4?file=/src/App.js
i need to show the item on the list when click on it, and this never happens because that condition never get true and i don't know why?
{id === itemId && el}
When you click the element, you successfully set the itemId value to the id of the clicked element:
const handleItemClick = id => {
setItemId(id);
};
However, upon re-rendering the component you dynamically generate all new id values:
id={v4()}
It's highly unlikely (re: impossible) that the newly generated UUID will be the same as the previously generated one.
Don't generate new UUID id values on every render. Either use static values or perhaps just an incrementing value from .map():
{arr.map((el, i) => {
return <Item
id={i}
itemId={itemId}
item={el}
handleItemClick={handleItemClick}
/>;
))}
Issue :
id={v4()} this will create a new uuid on each render, so when you click handleItemClick it update itemId and which cause re-render and all ids got changed due to id={v4()} so this will never get true {id === itemId && item}
{arr.map(el => {
return <Item
id={v4()} // <------ Issue is hear
itemId={itemId}
item={el}
handleItemClick={handleItemClick}
/>;
})}
Solution :
// instead of creating uuid on render you can create and save it as state, so it won't change on each re-render
const [items, setItems] = useState([1, 2, 3, 4].map(() => v4()));
{items.map(id => {
return (
<Item
id={id} // <----- Then use it like this
itemId={itemId}
item={id}
handleItemClick={handleItemClick}
/>
);
})}
WORKING DEMO :
First you missed the key prop, when using map(). It will just throw the warning, not to worry about much :P.
the main problem in your code is, v4 will generate random keys on each render. so
the id will never be the same as itemId. try to use some static id. if you can like the map comes with index. you may use that
arr.map((el,index) => {
return <Item
id={index}
key={el}
itemId={itemId}
item={el}
handleItemClick={handleItemClick}
/>;
});
I'm assuming v4 generates a random ID.
So id === itemId will never be true.
And I don't see itemId being initialized anywhere.
It would be great if you can tell me what is that condition for?
Or try {id === itemId || item}
Hope this helps
Thank you

how to stop rerendering in react on checkbox check?

I have simple list which is dynamically added on add button click. in my list there is a checkbox is also present .so I have an issue when I toggle the checkbox my whole list is re render why ?
let take example I added A,B,C,D in my list when I toggle D checkbox it should only render D item currently it render whole list why ?
here is my code
https://codesandbox.io/s/stupefied-wildflower-gv9be
const Item = ({ text, checked, onCheckedHandler }) => {
console.log(checked, "ssss");
return (
<div className={checked ? "bg" : ""}>
<span>{text}</span>
<input type="checkbox" onChange={e => onCheckedHandler(e, text)} />
</div>
);
};
Every time items changes (whether by adding a new item or checking a value), you are creating a new onCheckedHandler in your App. This propagates down to your Item component. Since the previous onCheckedHandler property is not referentially equivalent to the previous one, it renders (and you see that console log for each item). Memoizing the component alone won't help because a property being passed to it is changing every time.
To get around that, you need to memoize the onCheckedHandler, try this:
const onCheckedHandler = useCallback((e, selectedText) => {
const target = e.target
setItems(items => {
const i = items.findIndex(i => i.text === selectedText);
let obj = items[i];
obj.checked = target.checked;
return [...items.slice(0, i), obj, ...items.slice(i + 1)];
})
}, [setItems])
The you can wrap your Item compoennt with React.memo, and it should work as expected. You'll also need to import the useCallback the same way you import useState

React DatePicker loads with default value but then will not change when state changes

I have a component which renders x amount of DateInputs based on x number of charges. The date inputs should be rendered with the default value loaded from the server. Once the onChange is triggered it updates the state in the parent and prints out the console with the selected date using the computedProperty. To give more context here is an image of the page:
A you can see, a datepicker is rendered for each charge and the user needs the ability to modify the charge date. So in terms of data flow, the onChange in the child component triggers the method handleChargesDateChange in the parent, as the state needs to be stored in parent.
At the minute, the default charge date is rendered in the dropdown, but when onChange is executed then date input value does not change. However, the method prints out the correct date that was selected. Again, for more context, here is a screenshot of the data stored in parent when child executes onChange on the date picker.
As you can see, the date values get modified when the user selects one from each datepicker. However, the actual datepicker value stays the same. I can get it to work where it renders with an empty date for each datepicker and when the user selects a new date, it will render the datepicker with the selected date. It also stored the value in parent. But I am really struggling to render the defalt value, but when it is changed update the datepicker with the user selected value... I hope this makes sense.. Anyway here's the code. So the method which gets triggered in parent:
handleChargesDateChange = (e) => {
const date = e.target.value;
const momentDate = moment(date);
const chargeId = e.target.name;
console.log(date);
console.log(momentDate);
this.setState({
updatedChargeDates: [
...this.state.updatedChargeDates,
{[chargeId]: date}
]
}, () => console.log(this.state.updatedChargeDates))
}
This then gets passed down (around 3 children deep) via:
<Grid.Row>
<Grid.Column width={16}>
<OrderSummary
order={this.state.order}
fuelTypes={this.state.fuelTypes}
vehicleClasses={this.state.vehicleClasses}
deviceTypes={this.state.deviceTypes}
chargeTypes={this.state.chargeTypes}
featureTypes={this.state.featureTypes}
itemGroups={this.state.itemGroups}
itemTypes={this.state.itemTypes}
updateChargeDates={this.handleChargesDateChange}
chargeDates={this.state.updatedChargeDates}
/>
</Grid.Column>
I will skip the various other children components that the method gets passed to and go straight to the effected component. So the method then lands at Charges:
export class Charges extends Component {
constructor(props) {
super(props);
this.state = {
currentItem: 0,
newDate: '',
fields: {}
}
}
render() {
const {charges, chargeTypes, updateChargeDates, chargeDates} = this.props;
console.log('Props method', charges);
if (charges.length === 0) { return null; }
return <Form>
<h3>Charges</h3>
{charges.map(charge => {
console.log(charge);
const chargeType = chargeTypes.find(type => type.code === charge.type) || {};
return <Grid>
<Grid.Row columns={2}>
<Grid.Column>
<Form.Input
key={charge.id}
label={chargeType.description || charge.type}
value={Number(charge.amount).toFixed(2)}
readOnly
width={6} />
</Grid.Column>
<Grid.Column>
<DateInput
name={charge.id}
selected={this.state.newDate}
***onChange={updateChargeDates}***
***value={chargeDates[charge.id] == null ? charge.effectiveDate : chargeDates[charge.id]}***
/>
</Grid.Column>
</Grid.Row>
</Grid>
})}
<Divider hidden />
<br />
</Form>
}
I have highlighted the culprit code with *** to make it easier to follow. I am using a ternary type operator to determine which value to display, but it doesnt seem to update when i select a value.. I thought that from the code I have, if the initial chargeDates[charge.id] is null then we display the charge.effectiveDate which is what it is doing, but soon as I change the date i would expect chargeDates[charge.id] to have a value, which in turn should display that value...
I really hope that makes sense, im sure im missing the point with this here and it will be a simple fix.. But seems as im a newbie to react, im quite confused..
I have tride to follow the following resources but it doesnt really give me what I want:
Multiple DatePickers in State
Daepicker state
If you need anymore questions or clarification please do let me know!
Looking at your screenshot, updatedChargeDates (chargeDates prop) is an array of objects. The array has indexes 0, 1 and 2, but you access it in the DateInput like this chargeDates[charge.id] your trying to get the data at the index 431780 for example, which returns undefined, and so the condition chargeDates[charge.id] == null ? charge.effectiveDate : chargeDates[charge.id], will always return charge.effectiveDate.
You can fix this in handleChargesDateChange by making updatedChargeDates an object, like this :
handleChargesDateChange = (e) => {
const date = e.target.value;
const momentDate = moment(date);
const chargeId = e.target.name;
console.log(date);
console.log(momentDate);
this.setState({
updatedChargeDates: {
...this.state.updatedChargeDates,
[chargeId]: date
}
}, () => console.log(this.state.updatedChargeDates))
}

need something like event.target.focus() in React

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.

Categories

Resources