Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at BFusion / BFLEX 2010 (Bloomington, Indiana) with: Ed Bartram and Vaughn Bartram
Ben Nadel at BFusion / BFLEX 2010 (Bloomington, Indiana) with: Ed Bartram@edbartram ) and Vaughn Bartram

Is Using .bind() To Lock-In Arguments A "Code Smell" In ReactJS

By Ben Nadel on

We already know that we don't need to use the .bind() method for things like .forEach() and .map() in a ReactJS render() function. But, I still see .bind() being used to lock-in arguments at render time. This is often done to bind an event handler (like a click-handler) to a particular item within a virtual DOM collection. But, I wonder, is this a "code smell" in ReactJS? Does this approach go against the "React way?"

WARNING: This post is very much about "feelings" and not about "facts."


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

The "React way" isn't clearly codified in any one particular statement. But, each framework has its opinions on how problems should be solved. For the purposes of this post, I'm going to consider this passage from the "Thinking in React" documentation:

The first thing you'll want to do is to draw boxes around every component (and subcomponent) in the mock and give them all names. If you're working with a designer, they may have already done this, so go talk to them! Their Photoshop layer names may end up being the names of your React components!

But how do you know what should be its own component? Just use the same techniques for deciding if you should create a new function or object. One such technique is the single responsibility principle, that is, a component should ideally only do one thing. If it ends up growing, it should be decomposed into smaller subcomponents.

The big take-away here, for me, is the emphasis on the single responsibility principle for your components and subcomponents. This, combined with the one-way data flow, seems to be a big part of the "React Way."

Keeping that in mind, let's look at an example of how I sometimes see the .bind() method being used. In the following code, we have a list of friends, each with a "click count." If you click on any friend in the list, the click count for the selected friend is incremented. In the following code, take note of how .bind() is used to configure the click-handler:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Is Using .bind() To Lock-In Arguments A "Code Smell" In ReactJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Is Using .bind() To Lock-In Arguments A "Code Smell" In ReactJS
  • </h1>
  •  
  • <p>
  • <strong>Current Approach</strong>: Using <em>.bind( this, friend.id )</em>.
  • </p>
  •  
  • <div id="content">
  • <!-- This content will be replaced with the React rendering. -->
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script src="../../vendor/reactjs/react-0.13.3.js"></script>
  • <script src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script>
  • <script src="../../vendor/lodash/lodash-3.9.3.js"></script>
  • <script type="text/jsx">
  •  
  • // I manage the Demo widget.
  • var Demo = React.createClass({
  •  
  • // I return the initial state (and setup instance properties) for the component.
  • getInitialState: function() {
  •  
  • return({
  • friends: [
  • {
  • id: 1,
  • name: "Joanna",
  • clickCount: 0
  • },
  • {
  • id: 2,
  • name: "Sarah",
  • clickCount: 0
  • },
  • {
  • id: 3,
  • name: "Kim",
  • clickCount: 0
  • }
  • ]
  • });
  •  
  • },
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the click-event for a friend.
  • // --
  • // CAUTION: While this is an event-handler, the "event" argument is not the
  • // first argument because this method was bound using .bind() such that the
  • // friend ID could be "locked-in" at render time.
  • handleClick: function( friendID, event ) {
  •  
  • // Clone the data to make sure we aren't mutating values in-place.
  • var friends = _.clone( this.state.friends, true );
  •  
  • var friend = _.find(
  • friends,
  • {
  • id: friendID
  • }
  • );
  •  
  • friend.clickCount++;
  •  
  • this.setState({
  • friends: friends
  • });
  •  
  • },
  •  
  •  
  • // I return the virtual DOM based on the current state.
  • render: function() {
  •  
  • // Map the friends collection onto a virtual DOM collection.
  • // --
  • // NOTE: When we wire-up the onClick handler, notice that we are using
  • // the .bind() method to lock-in the "friend.id" as an argument to the
  • // click handler.
  • var friendsBlock = this.state.friends.map(
  • function operator( friend ) {
  •  
  • return(
  • <li key={ friend.id }>
  •  
  • <a onClick={ this.handleClick.bind( this, friend.id ) }>
  • { friend.name } ( { friend.clickCount } )
  • </a>
  •  
  • </li>
  • );
  •  
  • },
  • this
  • );
  •  
  • return(
  • <div>
  • <h2>
  • You have { this.state.friends.length } friends!
  • </h2>
  •  
  • <ul>
  • { friendsBlock }
  • </ul>
  • </div>
  • );
  •  
  • }
  •  
  • });
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // Render the root Demo and mount it inside the given element.
  • React.render( <Demo />, document.getElementById( "content" ) );
  •  
  • </script>
  •  
  • </body>
  • </html>

When the top-level component goes to render the virtual DOM (Document Object Model), it has to generate a list of friends. And, for each friend, it is binding a click-handler:

onClick={ this.handleClick.bind( this, friend.id ) }

In this case, we are using the Function.prototype.bind() method to partially apply the "friend.id" value as an argument. This way, when the event handler is invoked by the ReactJS event delegation system, we receive both the friend ID as well as the synthetic browser event.

So here's the "code smell." First off, this just feels wrong to me - which, I admit, is 100% opinion, not fact. Generally speaking, I try to avoid the .bind() method, which is usually easy to do with ReactJS' pre-bound methods. But, I don't want to go on "feelings" alone; I want to consider this in the light of the "React way."

NOTE: ReactJS doesn't pre-bind ES6 class methods.

Earlier, we talked about the single-responsibility principle as being part of the "React way." To me, using .bind() in this way violates this principle in two ways. First, it feels like we're overloading the concept of the event-handler. To me, the event handler should only be concerned with the event, not with the data. But, when we bind the data to the event handler, it "feels" like we're conflating two responsibilities.

Second, using .bind() in this way gives the top-level component too many responsibilities. Not only does the top-level component provide the business logic and render the collection of friends, it is also concerned with the item-level interactions and how those interactions feed back into the business logic. To me, it makes more sense - and feels more in alignment with the "React way" - to break the friends collection up into a collection of subcomponents, each of which is responsible for piping UI (user interface) events back into the business logic.

Here's what that might look like. In this version, the top-level component is rendering a collection of Friend components, each of which is given a friend instance and a view-model method to consume:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Is Using .bind() To Lock-In Arguments A "Code Smell" In ReactJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Is Using .bind() To Lock-In Arguments A "Code Smell" In ReactJS
  • </h1>
  •  
  • <p>
  • <strong>Current Approach</strong>: Using a <em>sub-component</em>.
  • </p>
  •  
  • <div id="content">
  • <!-- This content will be replaced with the React rendering. -->
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script src="../../vendor/reactjs/react-0.13.3.js"></script>
  • <script src="../../vendor/reactjs/JSXTransformer-0.13.3.js"></script>
  • <script src="../../vendor/lodash/lodash-3.9.3.js"></script>
  • <script type="text/jsx">
  •  
  • // I manage the Demo widget.
  • var Demo = React.createClass({
  •  
  • // I return the initial state (and setup instance properties) for the component.
  • getInitialState: function() {
  •  
  • return({
  • friends: [
  • {
  • id: 1,
  • name: "Joanna",
  • clickCount: 0
  • },
  • {
  • id: 2,
  • name: "Sarah",
  • clickCount: 0
  • },
  • {
  • id: 3,
  • name: "Kim",
  • clickCount: 0
  • }
  • ]
  • });
  •  
  • },
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I increment the click-count for the friend with the given ID.
  • incrementCount: function( friendID ) {
  •  
  • // Clone the data to make sure we aren't mutating values in-place.
  • var friends = _.clone( this.state.friends, true );
  •  
  • var friend = _.find(
  • friends,
  • {
  • id: friendID
  • }
  • );
  •  
  • friend.clickCount++;
  •  
  • this.setState({
  • friends: friends
  • });
  •  
  • },
  •  
  •  
  • // I return the virtual DOM based on the current state.
  • render: function() {
  •  
  • // Map the friends collection onto a virtual DOM collection. In this
  • // version, rather than using .bind(), we are going to render an instance
  • // of the Friend component, which will consume an exposed increment()
  • // method rather than a pre-bound event handler.
  • var friendsBlock = this.state.friends.map(
  • function operator( friend ) {
  •  
  • return(
  • <li key={ friend.id }>
  •  
  • <Friend
  • friend={ friend }
  • increment={ this.incrementCount }>
  • </Friend>
  •  
  • </li>
  • );
  •  
  • },
  • this
  • );
  •  
  • return(
  • <div>
  • <h2>
  • You have { this.state.friends.length } friends!
  • </h2>
  •  
  • <ul>
  • { friendsBlock }
  • </ul>
  • </div>
  • );
  •  
  • }
  •  
  • });
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I manage the Friend widget.
  • var Friend = React.createClass({
  •  
  • // I handle the click event on the component.
  • handleClick: function( event ) {
  •  
  • // Now that the Friend instance is implicitly linked to a given friend
  • // data point, we can easily consume the exposed increment() method
  • // without having to worry about binding arguments to methods.
  • this.props.increment( this.props.friend.id );
  •  
  • },
  •  
  •  
  • // I return the virtual DOM based on the current state.
  • render: function() {
  •  
  • return(
  • <a onClick={ this.handleClick }>
  • { this.props.friend.name } ( { this.props.friend.clickCount } )
  • </a>
  • );
  •  
  • }
  •  
  • });
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // Render the root Demo and mount it inside the given element.
  • React.render( <Demo />, document.getElementById( "content" ) );
  •  
  • </script>
  •  
  • </body>
  • </html>

Here, the top-level component simply tells the Friend component that an increment() method is available - it doesn't concern itself with how that method actually gets invoked; that's no longer its responsibility, it's the responsibility of the subcomponent.

To me, this approach feels cleaner and it feels much more like the "React way." But, like I said above, this is a matter of opinion. My opinion. And a reflection of how I like to write code. Your mileage may vary.




Reader Comments

This is a great blueprint for getting all that .bind(null, arg) crap out of otherwise clean React code. Thanks for the great post!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.