MaterialUI TextField lag issue. How to improve performance when the form has many inputs?

Viewed 7671

I encountered an annoying problem with Textfield using MateriaUI framework. I have a form with many inputs and it seems to be a bit laggy when typing or deleting values inside the fields. In other components when there are like 2 or 3 inputs there's no lag at all.

EDIT: The problem seems to be with my onChange handler.

Any help is much appreciated. Thanks in advance.

This is my custom input code:

import React, { useReducer, useEffect } from 'react';
import { validate } from '../utils/validators';
import TextField from '@material-ui/core/TextField';
import { ThemeProvider, makeStyles, createMuiTheme } from '@material-ui/core/styles';
import { green } from '@material-ui/core/colors';

const useStyles = makeStyles((theme) => ({
    root: {
        color: 'white'
    },
    input: {
        margin: '10px',
        '&& .MuiInput-underline:before': {
            borderBottomColor: 'white'
        },
    },
    label: {
        color: 'white'
    }
}));

const theme = createMuiTheme({
    palette: {
        primary: green,
    },
});


const inputReducer = (state, action) => {
    switch (action.type) {
        case 'CHANGE':
            return {
                ...state,
                value: action.val,
                isValid: validate(action.val, action.validators)
            };
        case 'TOUCH': {
            return {
                ...state,
                isTouched: true
            }
        }
        default:
            return state;
    }
};

const Input = props => {
    const [inputState, dispatch] = useReducer(inputReducer, {
        value: props.initialValue || '',
        isTouched: false,
        isValid: props.initialValid || false
    });

    const { id, onInput } = props;
    const { value, isValid } = inputState;

    useEffect(() => {
        onInput(id, value, isValid)
    }, [id, value, isValid, onInput]);

    const changeHandler = event => {
        dispatch({
            type: 'CHANGE',
            val: event.target.value,
            validators: props.validators
        });
    };

    const touchHandler = () => {
        dispatch({
            type: 'TOUCH'
        });
    };

    const classes = useStyles();

    return (
        <ThemeProvider theme={theme}>
            <TextField
                className={classes.input}
                InputProps={{
                    className: classes.root
                }}
                InputLabelProps={{
                    className: classes.label
                }}
                id={props.id}
                type={props.type}
                label={props.label}
                onChange={changeHandler}
                onBlur={touchHandler}
                value={inputState.value}
                title={props.title}
                error={!inputState.isValid && inputState.isTouched}
                helperText={!inputState.isValid && inputState.isTouched && props.errorText}
            />
        </ThemeProvider>
    );
};

export default Input;
3 Answers

In addition to @jony89 's answer. You can try 1 more workaround as following.

  1. On each keypress (onChange) update the local state.
  2. On blur event call the parent's change handler

       const Child = ({ parentInputValue, changeValue }) => {
      const [localValue, setLocalValue] = React.useState(parentInputValue);
      return <TextInputField
        value={localValue}
        onChange={(e) => setLocalValue(e.target.value)} 
        onBlur={() => changeValue(localValue)} />;

    }

    const Parent = () => {
        const [valMap, setValMap] = React.useState({
          child1: '',
          child2: ''
        });
        return (<>
            <Child parentInputValue={valMap.child1} changeValue={(val) => setValMap({...valMap, child1: val})}
            <Child parentInputValue={valMap.child2} changeValue={(val) => setValMap({...valMap, child2: val})}
        </>
        );
    }

This will solve your problems if you do not want to refactor the existing code.

But the actual fix would be splitting the state so that update in the state of child1 doesn't affect (change reference or mutate) state of child2.

Make sure to extract all constant values outside of the render scope.

For example, each render you are providing new object to InputLabelProps and InputProps which forces re-render of child components.

So every new object that is not must be created within the functional component, you should extract outside,

That includes :

        const touchHandler = () => {
            dispatch({
                type: 'TOUCH'
            });
        };
    
        const useStyles = makeStyles((theme) => ({
            root: {
                display: 'flex',
                flexWrap: 'wrap',
                color: 'white'
            },
            input: {
                margin: '10px',
                '&& .MuiInput-underline:before': {
                    borderBottomColor: 'white'
                },
            },
            label: {
                color: 'white'
            }
        }));
    
        const theme = createMuiTheme({
            palette: {
                primary: green,
            },
        });
    

Also you can use react memo for function component optimization, seems fit to your case.

I managed to get rid of this lagging effect by replacing normal <TextField /> by <Controller /> from react-hook-form.

Old code with Typing Lag

<Grid item xs={12}>
    <TextField
        error={descriptionError.length > 0}
        helperText={descriptionError}
        id="outlined-textarea"
        onChange={onDescriptionChange}
        required
        placeholder="Nice placeholder"
        value={description}
        rows={4}
        fullWidth
        multiline
    />
</Grid>

Updated Code with react-hook-form


import { FormProvider, useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';

const methods = useForm({
    resolver: yupResolver(validationSchema)
});
const { handleSubmit, errors, reset } = methods;

const onSubmit = async (entry) => {
    console.log(`This is the value entered in TextField ${entry.name}`);
};

<form onSubmit={handleSubmit(onSubmit)}>
    <Grid item xs={12}>
        <FormProvider fullWidth {...methods}>
            <DescriptionFormInput
                fullWidth
                name="name"
                placeholder="Nice placeholder here"
                size={matchesXS ? 'small' : 'medium'}
                bug={errors}
            />
        </FormProvider>
    </Grid>
    <Button
        type="submit"
        variant="contained"
        className={classes.btnSecondary}
        startIcon={<LayersTwoToneIcon />}
        color="secondary"
        size={'small'}
        sx={{ mt: 0.5 }}
    >
        ADD
    </Button>
</form>
import React from 'react';
import PropTypes from 'prop-types';
import { Controller, useFormContext } from 'react-hook-form';

import { FormHelperText, Grid, TextField } from '@material-ui/core';

const DescriptionFormInput = ({ bug, label, name, required, ...others }) => {
    const { control } = useFormContext();

    let isError = false;
    let errorMessage = '';
    if (bug && Object.prototype.hasOwnProperty.call(bug, name)) {
        isError = true;
        errorMessage = bug[name].message;
    }

    return (
        <>
            <Controller
                as={TextField}
                name={name}
                control={control}
                defaultValue=""
                label={label}
                fullWidth
                InputLabelProps={{
                    className: required ? 'required-label' : '',
                    required: required || false
                }}
                error={isError}
                {...others}
            />
            {errorMessage && (
                <Grid item xs={12}>
                    <FormHelperText error>{errorMessage}</FormHelperText>
                </Grid>
            )}
        </>
    );
};

With this use of react-hook-form I could get the TextField more responsive than earlier.

Related