React input onChange lag
Asked Answered
G

13

87

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?

Ghyll answered 12/6, 2018 at 14:9 Comment(0)
E
128

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
Emigrate answered 12/6, 2018 at 14:57 Comment(1)
If you are using the Formik library for your forms you can also take a look at Formik's <FastField /> component jaredpalmer.com/formik/docs/api/fastfieldTollmann
M
16

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

Mcgee answered 3/9, 2020 at 10:48 Comment(1)
This doesn't work for MUI components :(Suneya
T
13

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!

Taggart answered 19/4, 2021 at 15:10 Comment(0)
L
2

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.

Lampert answered 3/2, 2021 at 13:1 Comment(0)
L
2

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);
  };
Linden answered 28/8, 2021 at 20:29 Comment(2)
Yep, this is the best way to go. Can use use-debounce which has a nice hook wrapper.Astyanax
The “lastRequest” is a state or what? Where I have to declare it?Tristan
S
2

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.

Steck answered 19/1, 2023 at 22:25 Comment(0)
K
1

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.

Kannry answered 12/6, 2018 at 14:21 Comment(1)
"Note: As of React 16, react-addons-perf is not supported. Please use your browser’s profiling tools to get insight into which components re-render."Repose
A
1

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.

Acetabulum answered 14/8, 2020 at 15:55 Comment(0)
C
0

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.

Crustal answered 20/5, 2020 at 13:1 Comment(0)
W
0

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>
Westmoreland answered 27/3, 2022 at 13:31 Comment(0)
G
0

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);
Gamma answered 23/12, 2022 at 1:16 Comment(0)
R
0

I was having the same issue, what I did is to replace the

value => defaultValue
onChange => onBlur

Now in case of Editing you can have the value as defaultValue rendered in your input and onBlur will decrease the number of unnecessary re-renders so you will have good performance overall. You can keep the onChange in case you need the immediate value to do some other stuff. I hope this will help

<TextField
  value={this.state.itemNumber}
  onChange={this.handleChange}
>

to this

<TextField
  defaultValue={this.state.itemNumber}
  onBlur={this.handleChange}
/>
Reformer answered 8/3, 2023 at 5:51 Comment(0)
S
0

Stumble upon this and I solved it with this if anyone else want an easy solution. This makes it uncontrolled which is normally not what you want when using react. Be aware that you should probably also use ref but I have not seen any problems with just using it as below without ref, and I have been using it with hundreds of textfields:

<TextField
      id="Part #"
      label="Part #"
      defaultValue={this.state.itemNumber} //changed from value
      onBlur={this.handleChange} //changed from onChange
      margin="normal"
    />
Stipitate answered 6/12, 2023 at 11:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.