Last active
May 16, 2018 20:09
-
-
Save ryankshaw/1b55e4b63ccf65415ab3064a70471a9c to your computer and use it in GitHub Desktop.
how should I handle binding event listeners in react components?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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