React input onChange lag - reactjs

I have a simple controlled input with an onChange event handler.
Everything works as it should with handleChange firing on every keystroke, the problem is it is very slow.
There is a very noticeable lag when using the input. Is there some extra code I have to right to get this to work like a normal input?
Do I have to debounce the input?
There is no mention of this issue in the docs as far as I can tell, and I don't know if there's something extra I have to do or if I'm using the onChange callback incorrectly.
handleChange = (event) => {
this.setState({ itemNumber: event.target.value })
}
<TextField
id="Part #"
label="Part #"
value={this.state.itemNumber}
onChange={this.handleChange}
margin="normal"
/>
The component:
export class Dashboard extends Component {
state = {
report: '',
selectedDate: new Date(),
itemNumber: '',
}
static propTypes = {
classes: object,
headerTitle: string,
userInfo: object,
}
static defaultProps = {
classes: {},
headerTitle: undefined,
userInfo: {},
}
reportSelected = (event) => {
this.setState(() => {
return {
report: event.target.value,
}
})
}
handleDateChange = (date) => {
this.setState({ selectedDate: new Date(date) })
}
handleChange = (event) => {
this.setState({ itemNumber: event.target.value })
}
render () {
const { classes, headerTitle, userInfo } = this.props
return (
<div className={classes.dashboard}>
<HeaderTitle title="Dashboard" />
<Helmet>
<title>{headerTitle}</title>
</Helmet>
{ userInfo.isAuthorized &&
<Grid container direction={'row'} justify={'center'} className={classes.formContainer}>
<Grid item xs={12} sm={12} md={12} lg={6} xl={5}>
<form className={classes.form}>
<FormControl className={classes.presetReportsInput}>
<InputLabel htmlFor="reports">Preset Reports</InputLabel>
<Select
value={this.state.report}
onChange={this.reportSelected}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{presetReports.getReportList().map(report => (
<MenuItem value={report.name} key={report.name}>
{report.name}
</MenuItem>
))}
</Select>
</FormControl>
{ (this.state.report === 'Inventory Snapshot' ||
this.state.report === 'Weekly Fill Rate' ||
this.state.report === 'Open Orders' ||
this.state.report === 'Weekly Shipments') &&
<div>
<Grid container spacing={8} direction={'row'}>
<Grid item>
<MuiPickersUtilsProvider utils={MomentUtils}>
<DatePicker
className={classes.datePicker}
margin="normal"
keyboard
format="DD/MM/YYYY"
disableFuture
autoOk
mask={value => (value ? [/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/] : [])}
value={this.state.selectedDate}
onChange={this.handleDateChange}
disableOpenOnEnter
animateYearScrolling={false}
/>
</MuiPickersUtilsProvider>
</Grid>
<Grid item>
<TextField
id="Part #"
label="Part #"
value={this.state.itemNumber}
onChange={this.handleChange}
margin="normal"
/>
</Grid>
</Grid>
<Button variant="raised" color="primary" style={{ marginTop: 10 }}>
Search
</Button>
</div>
}
{ this.state.report === '' &&
<div>
<TextField
id="queryField"
label="Run a Query"
className={classes.queryField}
helperText=""
margin="normal"
multiline
rows="5"
/>
<Grid container direction={'row'} justify={'flex-end'}>
<Grid item>
<Button variant="raised" color="primary">
Export
</Button>
</Grid>
<Grid item>
<Button variant="raised" color="primary">
Save Query
</Button>
</Grid>
</Grid>
</div>
}
</form>
</Grid>
{ this.state.report === 'Inventory Snapshot' &&
<Grid container className={classes.table}>
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
<InventoryReport />
</Grid>
</Grid>
}
</Grid>
}
</div>
)
}
}
const styles = {
dashboard: {},
formContainer: {
margin: 0,
width: '100%',
},
presetReportsInput: {
width: '100%',
margin: '20% 0 0 0',
},
queryField: {
width: '100%',
margin: '20% 0 0 0',
},
table: {
margin: '50px 0 10px 0',
},
datePicker: {
marginTop: 32,
},
}
const mapStateToProps = state => {
const { layout } = state
const { headerTitle } = layout
return {
headerTitle: headerTitle,
}
}
export default connect(mapStateToProps)(withStyles(styles)(Dashboard))
I'm watching the state update in react devtools in chrome and there's at least a 500ms lag between a character being entered and the state updating, much longer for faster typing. Why is setState so slow? What's the workaround to getting this form to behave like a normal web form?

setState by itself is not slow, it is only when your renders get very expensive that it starts causing issues.
A few things to consider are:
Your main component seems quite large and is being re-rendered on every keystroke so that might cause performance issues. Try breaking it down to smaller components.
Ensure the child-components being rendered in the render method of your main component do not get unnecessarily re-rendered. React Developer Tools or why-did-you-render can point out those unnecessary rerenders. Switching to PureComponent, stateless components or using shouldComponentUpdate can help.
While you can't avoid rerendering here (since your form inputs need to rerender with the new state values), by breaking into smaller components you can look to use shouldComponentUpdate to let React know if a component’s output is not affected by the current change in state or props and avoid unnecessarily rerendering.
If you use functional components:
Use useMemo to prevent recomputing expensive operations or components unless some dependency changed.
Use useCallback to return a memoized version of a callback so that child components that rely on reference equality don't unnecessarily rerender
Use React.memo if your functional component renders the same result given the same props to prevent it from unnecessarily rerendering. Use the second argument to React.memo to customize the behaviour of the memoization (similar to shouldComponentUpdate)
Switch to the production build to get better performance
Switch to uncontrolled components and let the DOM handle the input components itself (this is the "normal web form" behaviour you described). When you need to access the form's values you can use ref's to get access to the underlying DOM nodes and read the values directly off that. This should eliminate the need to call setState and therefore rerender

This is especially an issue for big forms when a change in one input triggers the whole component re-render and that too in redux, although redux does not seem to be interfering here.
If you want your inputs to be controlled and have the same exact behaviour then you can always do like this
<input
className="form-control"
type="text"
name="name"
value={form.name}
onBlur={onChangeHandler}
/>
This only triggers an event on blur and prevent re-render on each change. It's useful since when you click on any other button to process the data, it's guaranteed that you'll have the updated state.
This will not be helpful if your logic requires instant validation/processing related to the input data.
Also, It's important I should mention that sometimes this fails to work with other components which are not native to HTML5 since they might prevent a re-render based on the value prop
Note: Please read the onBlur event here

This might seem like a trivial response but — make sure your console is closed. There is a noticeable lag in controlled components when the console is open!

if you're using a big parent component with too many re-render dependencies
I recommend you to handle re-renders in useEffect of child components
or if its force to use all state updates in the parent
use debounce in the child components.

Options given by #ᴘᴀɴᴀʏɪᴏᴛɪs are good but for my use case it didn't help. I solved it by adding a debounce before setting the state.
const delaySetStateWithDelay = () => {
if (lastRequest) {
window.clearTimeout(lastRequest);
}
lastRequest = setTimeout(() => {
setStateData('Data I Need To Set'
}, 500);
};

You could use https://reactjs.org/docs/perf.html to profile your app. Do you have a large number of components which could be getting re-rendered? It might be necessary to add some componentShouldUpdate() methods to your components to prevent useless re-renders.

I recently faced the same problem, I had a redux store and a description in it, after every keystroke in the description input the store had to be updated, I tried to debounce in lodash but it didn't work, so I created a set timeout function to simply update the store state like this,
setTimeout(() => {
console.log("setting");
this.props.addDescription(this.state.description);
}, 200);
I take the description input field value from the description components' own state and whenever it re-renders I use componentDidMount() to get the latest updated description value from the store.

I had similar problem with slow input in development mode, but with functional components and hooks. Production was ok, but obviously switching on productions doesn't look like approach, if it so slow in development mode there is high chance some problem with code exists. So solution was to isolate state of input from the rest components. State that used by component should be available only for this component. Actually it's not even a solution, but how things should be in react.

I was having the same problem a while ago, had to copy the state coming from a parent component to a local object and then reference the new local object to my input.
If you don't need to use the new state anywhere else prior to saving the form, I reckon you can use the solution below.
const { selectedCustomer } = props;
const [localCustomer, setLocalCustomer] = useState({ name: 'Default' });
useEffect(() => {
setLocalCustomer({ ...selectedCustomer });
}, [selectedCustomer]);
const handleChangeName = (e) => {
setLocalCustomer({ ...localCustomer, name: e.target.value });
};
and then use it in my text field.
<StyledTextField
fullWidth
type='text'
label='Name'
value={localCustomer.name}
</StyledTextField>

Just adding a quick setTimeout was enough to improve performance a lot. The concern I had about timeouts of 100ms or longer was that if submit was executed, depending on the implementation, setState data may not have been added yet.
Something like below should prevent this happening in any real situation:
const onChange = (mydata) => setTimeout(() => setState(mydata), 10);

as noted in #Sudhanshu Kumar's post above, you can use 'onBlur' to execute the setState call when the input element loses focus by passing onChange handler as a callback. To get this to work with MUI use the following...
<TextField
...props
inputProps={{
onBlur: handleChange
}}
/>
This allows for override of native browser onBlur method. Hope this helps.

Related

How to show content when click on button

I have a button that I would like when the user clicks on it to show what is inside my MaterialTextField. Inside my materialtextfield's there is: value={projectFilter.eventName}, {projectFilter.eventYear}, and others that follow the same logic. How can I make it so that I have this result?
UPDATE(here some example of what I want to show when I click the button):
return (
<Grid item xs={4}>
<MaterialTextField
autoComplete="off"
variant="outlined"
label="Name"
type="text"
name="eventName"
value={projectFilter.eventName}
sx={{ width: '100%' }}
/>
</Grid>
)
Please provide some code sample.
Basically you have to set state which will change when user clicks the button.
https://reactjs.org/docs/state-and-lifecycle.html
You need state to store the current value of the field. You can use the useState hook for this. You should really read through the React documentation. The useState hook docs in particular is what you are after but it will make much more sense if you read state and lifecycle and just understand how react works.
Here is an example assuming that this projectFilter object is passed into the component as a prop.
import React from 'react';
export const SomeComponent = ({ projectFilter }) => {
const [fieldValue, setFieldValue] = React.useState('');
const clickHandler = (e) => {
// Set the value of the state here. This will cause a re-render.
setFieldValue(projectFilter.eventName);
};
return (
<Grid item xs={4}>
<MaterialTextField value={fieldValue} />
<button onClick={clickHandler}>Click this</button>
</Grid>
);

React testing by input type

I am trying to test my below component in which I am returning dynamic control based upon the switch condition:
const DataTableFormControl: FC<DataTableFormControlProp> = ({ displayName, isRequired, dataType, content, cellId }) => {
return (
(() => {
switch (dataType) {
case DataType.NUMBER:
return <TextField
id={cellId}
label={displayName}
defaultValue={content}
required={isRequired}
type="number"
/>
case DataType.DATE:
return <TextField
id={cellId}
label={displayName}
defaultValue={content}
type="date"
required={isRequired}
/>
case DataType.BOOLEAN:
return <FormControl variant="standard">
<Typography>{displayName}</Typography>
<Switch color="primary" defaultChecked={content === 'true'} />
</FormControl>
case DataType.PERCENT:
return <FormControl variant="standard">
<Typography>{displayName}</Typography>
<Input
id={cellId}
defaultValue={content}
required={isRequired}
endAdornment={<InputAdornment position="end">%</InputAdornment>}
/>
</FormControl>
default:
return <TextField
id={cellId}
label={displayName}
type="text"
required={isRequired}
defaultValue={content}
/>
}
})()
)
};
export { DataTableFormControl };
I tried below way to find the item but, how I can run an assertion based upon the TextField type and check if we are getting or have endAdornment property?
describe('DataTableFormControl', () => {
test('should render component with text dataType', () => {
const { container } = renderControl(DataType.TEXT);
const textElement = container.querySelector(`#${cellId}`);
expect(textElement).toBeInTheDocument();
// How we can run find if this element is type of text, date, or number.
});
});
In general React testing library discourages testing props just for the sake of checking that props passed to a component. The whole idea of React testing library is writing tests that translate to "how this is effecting the user?". In other words, a better test would check how does the existence of a prop affect the DOM (and by extension, the user), instead of just checking if a prop was passed or not.
I would also recommend avoiding using querySelector and to use getBy... queries which should be able to find any item you are looking for. In your case, since you are looking for inputs getByRole would be best. Depending on the type of the input you can do getByRole('textbox'), getByRole('checkbox') and so on.

MUI Autocomplete and react-hook-form not displaying selected option with fetched data

I have a MUI Autocomplete inside a form from react hook form that works fine while filling the form, but when I want to show the form filled with fetched data, the MUI Autocomplete only displays the selected option after two renders.
I think it's something with useEffect and reset (from react hook form), because the Autocompletes whose options are static works fine, but the ones that I also have to fetch the options from my API only works properly after the second time the useEffect runs.
I can't reproduce a codesandbox because it's a large project that consumes a real api, but I can provide more information if needed. Thanks in advance if someone can help me with this.
The page where I choose an item to visualize inside the form:
const People: React.FC = () => {
const [show, setShow] = useState(false);
const [modalData, setModalData] = useState<PeopleProps>({} as PeopleProps);
async function showCustomer(id: string) {
await api
.get(`people/${id}`)
.then((response) => {
setModalData(response.data);
setShow(true);
})
.catch((error) => toast.error('Error')
)
}
return (
<>
{...} // there's a table here with items that onClick will fire showCustomer()
<Modal
data={modalData}
visible={show}
/>
</>
);
};
My form inside the Modal:
const Modal: React.FC<ModalProps> = ({data, visible}) => {
const [situations, setSituations] = useState<Options[]>([]);
const methods = useForm<PeopleProps>({defaultValues: data});
const {reset} = methods;
/* FETCH POSSIBLE SITUATIONS FROM API*/
useEffect(() => {
api
.get('situations')
.then((situation) => setSituations(situation.data.data))
.catch((error) => toast.error('Error'));
}, [visible]);
/* RESET FORM TO POPULATE WITH FETCHED DATA */
useEffect(() => reset(data), [visible]);
return (
<Dialog open={visible}>
<FormProvider {...methods}>
<DialogContent>
<ComboBox
name="situation_id"
label="Situação"
options={situations.map((item) => ({
id: item.id,
text: item.description
}))}
/>
</DialogContent>
</FormProvider>
</Dialog>
);
};
export default Modal;
ComboBox component:
const ComboBox: React.FC<ComboProps> = ({name, options, ...props}) => {
const {control, getValues} = useFormContext();
return (
<Controller
name={`${name}`}
control={control}
render={(props) => (
<Autocomplete
{...props}
options={options}
getOptionLabel={(option) => option.text}
getOptionSelected={(option, value) => option.id === value.id}
defaultValue={options.find(
(item) => item.id === getValues(`${name}`)
)}
renderInput={(params) => (
<TextField
variant="outlined"
{...props}
{...params}
/>
)}
onChange={(event, data) => {
props.field.onChange(data?.id);
}}
/>
)}
/>
);
};
export default ComboBox;
I think you simplify some things here:
render the <Modal /> component conditionally so you don't have to render it when you are not using it.
you shouldn't set the defaultValue for your <Autocomplete /> component as RHF will manage the state for you. So if you are resetting the form RHF will use that new value for this control.
it's much easier to just use one of the fetched options as the current/default value for the <Autocomplete /> - so instead of iterating over all your options every time a change is gonna happen (and passing situation_id as the value for this control), just find the default option after you fetched the situations and use this value to reset the form. In the CodeSandbox, i renamed your control from "situation_id" to "situation". This way you only have to map "situation_id" on the first render of <Modal /> and right before you would send the edited values to your api on save.
I made a small CodeSandbox trying to reproduce your use case, have a look:
mui#v4
mui#v5
Another important thing: you should use useFormContext only if you have deeply nested controls, otherwise just pass the control to your <ComboBox /> component. As with using FormProvider it could affect the performance of your app if the form gets bigger and complex. From the documentation:
React Hook Form's FormProvider is built upon React's Context API. It solves the problem where data is passed through the component tree without having to pass props down manually at every level. This also causes the component tree to trigger a re-render when React Hook Form triggers a state update

Updating react state in parent very slow

I'm developing a react application, and where I always thought the react state updates were really fast, I now found a problem.
I have a page view with a lot of elements on it, one of the elements is this one that gets loaded in the page:
<NotesCard notes={deal.notes} updateNotes={notes => {setDeal(prevState => ({...prevState, notes}))}} />
NotesCard is a child component that only renders a material-ui Card with another react component in:
export default function NotesCard(props) {
const {notes, updateNotes} = props;
return (
<Card className="Card">
<CardHeader
title="Notities"
/>
<CardContent>
<Notes notes={notes} onChange={updateNotes} />
</CardContent>
</Card>
);
}
Notes is the last component that renders a text field and just takes the props to the TextField:
function Notes(props) {
const {notes} = props;
function updateNotes(event) {
// props.deal.notes = event.target.value;
props.onChange(event.target.value);
}
return (
<div>
<FormGroup>
{notes !== null ?
<TextField
multiline
defaultValue={notes}
onChange={e => updateNotes(e)}
rows={3}
variant={'outlined'}
label={'Notities'}
/>
: 'Geen beschrijving...'}
</FormGroup>
</div>
);
}
Is there anything that I do wrong that creates a lot of lag? The page is a big page so it might have something to do with that, but I'd think that the updates performances would still be okay.
Your goal is to fix the slow render, only then you will want to take a look at number of rerenders if necessary.
Please install the react-dev-tools which contains an option to mark components when the are being rerendered. Alternativly you can also monitor the performance over a couple of seconds and investigate the rendering. This should help you understand what renders unnecessarily on your actions.
I see a potential problem with this one:
<Notes notes={notes} onChange={updateNotes} />
If you trigger onChange the parent state is mutated. This then causes ALL children to rerender. I would think that only the single will change and a change in this component wont effect other siblings. So try to move the state as close to where its used as possible. If you trigger onChange only the should be updated. This is something easy which can fix a ton of performance problems without using react features like Memo.

Autocomplete - MaterialUI - Controled component doesn't work

Basically, I created the autocomplete component and set default value as my state this.state.quest.ansType, but when I change this state, the component result an error: component is changing the default value state of an uncontrolled Autocomplete after being initialized. To suppress this warning opt to use a controlled Autocomplete.
I need this for my update function. When the user selects the register on the database, I will load the options saved on the default value of Autocomplete.
const ansTypes = [
{
id: 'T',
desc: "Texto"
},
{
id: 'M',
desc: "Multipla Escolha"
},
{
id: 'U',
desc: "Escolha Única"
},
];
<Autocomplete className="cb" id={"ansType"} options={ansTypes}
disableCloseOnSelect
onChange={obj => this.selectAnsType(obj)}
defaultValue={this.state.quest.ansType}
getOptionLabel={option => option.desc}
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox
style={{ marginRight: 8 }}
checked={selected}
color={"primary"}
/>
{option.desc}
</React.Fragment>
)}
renderInput={(params) => (<TextField {...params} label={"Answer Type"} />)}
/>
Basically, you've been changing the default value after the first render, which shouldn't happen. Either you use an uncontrolled Component (-onChange, -value,+defaultValue, +ref) of you use a controlled component (+onChange, +value, -defaultValue, ref->only if needed).
DefaultValue should be used for the uncontrolled ones!
Controlled
...
<Autocomplete className="cb" id={"ansType"} options={ansTypes}
...
onChange={obj => this.selectAnsType(obj)}
value={this.state.quest.ansType}
...
/>
Uncontrolled
...
defaultAnsType={...};
myUncontrolledAutocomplete=React.createRef();
<Autocomplete className="cb" id={"ansType"} options={ansTypes}
...
defaultValue={this.defaultAnsType}
ref={this.myUncontrolledAutocomplete}
...
/>
$
Personal Opinion
I'd use the controlled one, because it's easier to understand. On the other hand the uncontrolled component could make your component stateless which could be something you might wanna use in the future.

Resources