loading...

Quick Tip - Memoizing change handlers in React Components

robertbroersma profile image Robert Updated on ・2 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 only 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 }))
}), [])

Discussion

pic
Editor guide
Collapse
dawsonbotsford profile image
Daws Bot

Great article, I think this is mandatory knowledge for intermediate React developers. With the introduction of hooks, how might you add to this example for both memoizing a handler AND avoid recreating that memoized function on each render?

If I'm understanding hooks properly, this tutorial would not work.

Collapse
robertbroersma profile image
Robert Author

Hey Dawson! Thanks for the reply. I realize this might be a bit late, but I think you could use a combination of useCallback and memoize:

const [state, setState] = useState(initialValues)

const handleChange = useCallback(memoize(key => value => {
  setState({ [key]: value })
}), [])

Perhaps there's a better or easier way using useReducer!