Skip to content

Instantly share code, notes, and snippets.

@ryankshaw
Last active May 16, 2018 20:09
Show Gist options
  • Save ryankshaw/1b55e4b63ccf65415ab3064a70471a9c to your computer and use it in GitHub Desktop.
Save ryankshaw/1b55e4b63ccf65415ab3064a70471a9c to your computer and use it in GitHub Desktop.
how should I handle binding event listeners in react components?
// bad: manual binding in constructor
class Foo extends React.Component {
// this is a lot of boilerplate that does not really give us any insight into
// what we are intending to do with our code
constructor(props) {
super(props)
this.updateCounter = this.updateCounter.bind(this)
this.state = {
counter:0
}
}
updateCounter(e) {
e.preventDefault()
this.setState(state => ({counter: state.counter + 1}))
}
render() {
return (
<a onClick={this.updateCounter} href="#">increment counter</a>
)
}
}
//////////////////////////////////////////////////////////////////////
// Attempt 2, Also bad: with propertyInitializers
class Foo extends React.Component {
// by moving 'state' and 'updateCounter' to class property intializers
// we can remove all that "constructor" boilerplate, but now we broke `super`
// and have to deal with all the subtleties of how the behavior is actually
// different than before like this talks about:
// https://medium.com/@charpeni/arrow-functions-in-class-properties-might-not-be-as-great-as-we-think-3b3551c440b1
state = {
counter:0
}
updateCounter = e => {
e.preventDefault()
this.setState(state => ({counter: state.counter + 1}))
}
render() {
return (
<a onClick={() => this.updateCounter()} href="#">increment counter</a>
)
}
}
//////////////////////////////////////////////////////////////////////
// attempt 3, better: but "preventDefault" still leaks into our public API
class Foo extends React.Component {
state = {
counter:0
}
render() {
// by just using an annonomous function here, we don't even need to worry about
// binding to this. so we get the best of both worlds.
return (
<a onClick={e => this.updateCounter(e)} href="#">increment counter</a>
)
}
// But why even have this accept an "event" argument? That means to test it you have
// to do silly things like myComponent.updateCounter({preventDefault: sinon.stub()})
// That is an implementation detail of the fact it was a listener of an <a> that
// now leaked from the DOM into our component's public API --
// just so we can call .preventDefault() on it.
updateCounter(e) {
e.preventDefault()
this.setState(state => ({counter: state.counter + 1}))
}
}
//////////////////////////////////////////////////////////////////////
// attempt 4, Best:
import preventDefault from 'prevent-default' // or from 'compiled/fn/preventDefault' if you are already in canvas
class Foo extends React.Component {
// no boilerplate, no groking the subtleties of property intializers
state = {
counter:0
}
// no DOM leakage here. updateCounter isn't concerned about an "event" object
updateCounter() {
this.setState(state => ({counter: state.counter + 1}))
}
render() {
/*
you might say "well this onclick looks more verbose now"
but I would say:
1.it is now explicit right here by reading this render method that yes,
we are making sure to preventDefault() on this <a> when it is clicked
because it says so right in the context where we see that it is an <a>
2. if designs call for this to become a <button> instead of an <a>,
we don't have to change our public api, and all the tests that test this
from myComponent.updateCounter({preventDefault: sinon.stub()}) to myComponent.updateCounter()
3. preventingDefault *is* a concern of DOM markup, so it *should* be here
*/
return (
<a onClick={preventDefault(() => this.updateCounter())} href="#">increment counter</a>
)
}
}
// Even in the case where we *care* about something from the event object, I think that is a
// detail that belongs in the markup and not in or component's API, eg:
// BAD:
class Foo extends React.Component {
// we shouldn't care about "event" objects here, what we care about is the new name they want to set.
handleNameChange(event) {
const newName = event.target.value
this.setState({name: newName})
}
render() {
return (
<input
value={this.state.name}
onChange={e => this.handleNameChange(e)}
/>
)
}
}
// somewhere else (like in a spec):
myFoo.handleNameChange({
target: {
value: 'Bill'
}
})
// GOOD:
class Foo extends React.Component {
handleNameChange(newName) {
this.setState({name: newName})
}
render() {
return (
<input
value={this.state.name}
onChange={e => this.handleNameChange(e.target.value)}
/>
)
}
}
// somewhere else (like in a spec):
myFoo.handleNameChange('Bill')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment