Quick Tip - Memoizing change handlers in React Components

ยท

3 min read

Let's consider a basic form with a controlled component in react:

class Form extends React.Component {
  state = {
    value: '',
  };

  handleChange = e => {
    this.setState({
      value: e.target.value,
    });
  };

  render() {
    return (
      <div>
        <InputComponent type="text" value={this.state.value} onChange={this.handleChange} />
      </div>
    )
  }
}

We keep a state, pass the value to our InputComponent, and update the value with the value we get from it.

Now consider this larger form. I like to use this arrow-function-that-returns-another-arrow-function (what do you call this?) syntax for brevity and to not have to repeat myself with multiple change handlers.

class BiggerForm extends React.Component {
  state = {
    a: '',
    b: '',
    c: '',
  };

  handleChange = key => e => {
    this.setState({
      [key]: e.target.value,
    });
  };

  render() {
    return (
      <div>
        <InputComponent type="text" value={this.state.a} onChange={this.handleChange('a')} />
        <InputComponent type="text" value={this.state.b} onChange={this.handleChange('b')} />
        <InputComponent type="text" value={this.state.c} onChange={this.handleChange('c')} />
      </div>
    )
  }
}

Looks easy, right? The problem with this is that this.handleChange() will create a new function every time it is called. Meaning everytime BiggerForm re-renders, all the InputComponents will re-render. Meaning EVERYTHING will re-render on EVERY keystroke. You can imagine what this would do to a huge form.

Now what we could do is either split handleChange into specific change handlers, e.g. handleChangeA, handleChangeB, handleChangeC, and this would solve our issue. But this is a lot of repetition, and considering we are considering huge forms; a lot of tedious work.

Luckily there's this thing called memoization! Which in short is a caching mechanism for our functions. Sounds fancy, but all it does is remember which arguments yield what result when calling a function. When the function is called again with the same arguments, it will not execute the function, but just return the same result. In our example:

class MemoizeForm extends React.Component {
  state = {
    a: '',
    b: '',
    c: '',
  };

  handleChange = memoize(key => e => {
    this.setState({
      [key]: e.target.value,
    });
  });

  render() {
    return (
      <div>
        <InputComponent type="text" value={this.state.a} onChange={this.handleChange('a')} />
        <InputComponent type="text" value={this.state.b} onChange={this.handleChange('b')} />
        <InputComponent type="text" value={this.state.c} onChange={this.handleChange('c')} />
      </div>
    )
  }
}

That was easy! In this example, on the first render of MemoizeForm, the handleChange function is called for every InputComponent with their specific key as an argument. Whenever MemoizeForm re-renders, handleChange is called again. However, since it's called with the same argument as before, the memoization mechanism returns the same function (with the same reference), and the InputComponent is not re-rendered (unless the value is changed ofcourse!).

๐ŸŽ‰

P.S. Any memoization library will do, I like to use fast-memoize

-- EDIT --

I've recently learned that event.target contains a lot more stuff! Using hooks you could just do:

const [state, setState] = useState(initialValues)

const handleChange = useCallback(e => {
  setState(values => ({ ...values, [e.target.name]: e.target.value }))
}), [])