Quick Tip - Memoizing change handlers in React Components
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 }))
}), [])