loading...
Cover image for Stahhp With The Outdated React Techniques!

Stahhp With The Outdated React Techniques!

bytebodger profile image Adam Nathaniel Davis ・6 min read

As an admitted React acolyte, I've written almost nothing but function-based components for the last six months or so. But I still have many thousands of LoC to my name in class-based components. And I'm getting really tired of seeing people showing examples of class-based components - circa 2014 - and then using those hackneyed examples as putative "proof" that classes are inferior.

If you can't write a modern example of what a class-based component should look like, then please don't purport to educate others in the matter.

To be absolutely clear, I'm not fighting "for" class-based components. Switch to Hooks, if you like. I have. But don't pull up ridiculous examples of class-based components as the basis for your comparison.


Alt Text

The Suspect

Here's what I'm talking about. I recently saw this exact example shown in another article on this site:

class Counter extends Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };

    this.increment = this.increment.bind(this);
  }

  increment() {
    this.setState({ count: this.state.count + 1});
  }

  render() {
    return (
      <div>
        <button onClick={this.increment}>add 1</button>
        <p>{this.state.count}</p>
      </div>
    );
  }
}

As always seems to be the case, this hackneyed example is used as supposed evidence of the verbosity and complexity of class-based components. Predictably, the next example shows the same component done with Hooks. And of course, it's much shorter, and presumably easier, to learn.

The problem is that the class-component shown above is bogus. Here's why:


Alt Text

Stop Binding Everything!

This was a necessary step when React was first introduced - more than a half decade ago. It's not necessary anymore with modern JavaScript.

Instead, we can declare our methods statically with the arrow syntax. The revised example looks like this:

class Counter extends Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1});
  }

  render = () => {
    return (
      <div>
        <button onClick={this.increment}>add 1</button>
        <p>{this.state.count}</p>
      </div>
    );
  }
}

[NOTE: I realize that, even amongst those who declare their methods this way, they often declare the traditional render method as render() {...}. I personally prefer to declare the render method in the same way that I declare the rest of my methods, so everything is consistent.]

You might be thinking that this isn't much of a change. After all, I only eliminated a single LoC. But there are key benefits to this approach:

  1. Modern JavaScript is becoming ever more replete with functions declared by arrow syntax. Therefore, declaring the class "methods" (which are really just... functions) with this syntax is more modern, more idiomatic, and it keeps all of the code more consistent.

  2. This method removes the boilerplate we've grown numb to at the top of many old-timey class-based React components where all the functions are bound in the constructor. It also avoids development blunders when you try to invoke a function and realize that you've forgotten to bind it in the constructor. Removing boilerplate is almost always a net-good.

  3. Class functions declared in this way cannot be accidentally redefined at runtime.

  4. Removing bindings from the constructor clears the way for us to remove other things from the constructor...

Alt Text

Stop Constructing Everything!

When writing class-based components, there are definitely times when a constructor is necessary. But those times are... rare.

Look, I get it. Every React tutorial since 1963 has used constructors. So it's understandable why this paradigm is still being flogged to death. But a constructor (in a React component) is almost always confined to two purposes:

  1. Initializing state variables

  2. Binding methods

Well, we already removed the bindings from the constructor. So that only leaves the initialization of the state variables. But you almost never need to initialize those variables inside of a constructor. You can simply declare them as part of the state object. Right at the top of your class body.

class Counter extends Component {
  state = { count: 0 };

  increment = () => {
    this.setState({ count: this.state.count + 1});
  }

  render = () => {
    return (
      <div>
        <button onClick={this.increment}>add 1</button>
        <p>{this.state.count}</p>
      </div>
    );
  }
}

Look at that! No bindings. No constructor. Just a clean, simple initialization of state.


Alt Text

Stop Writing Needless Tomes!

I realize that this point is going to veer heavily into the touchy ground of coding style. And believe me, there are many aspects of my code that I choose to make purposely verbose. But if we really wanna compare apples-to-apples when it comes to class-based or function-based components, we should strive to write both as succinctly as possible.

For example, why does increment() have its own bracketed set of instructions when there's only one line of code in the function??

(And yeah... I know there's an argument to be made that you pick one convention for your functions and you stick to it. But if you know that a given function will only ever do one thing - with a single line of code, then it feels rather silly to me to keep buffeting that single line of code in brackets. I find this especially important when you're trying to compare two programming paradigms based on their putative verbosity and complexity.)

So we can slim down our component like so:

class Counter extends Component {
  state = { count: 0 };

  increment = () => this.setState({ count: this.state.count + 1});

  render = () => {
    return (
      <div>
        <button onClick={this.increment}>add 1</button>
        <p>{this.state.count}</p>
      </div>
    );
  }
}

But we don't need to stop there.

Class-based components often look a bit longer because they're required to have a render() function, which in turn, returns some form of JSX. But it's quite common for a component's render() function to contain no other logic other than the return of the display values. This means that, in many class-based components, the render() function can be slimmed down like this:

class Counter extends Component {
  state = { count: 0 };

  increment = () => this.setState({ count: this.state.count + 1});

  render = () => (
    <div>
      <button onClick={this.increment}>add 1</button>
      <p>{this.state.count}</p>
    </div>
  );
}

Now compare this example to the bloated code that was originally offered as a template for class-based components. Quite a bit shorter, no?? And is it any harder to read? Obviously, that's subjective, but I don't think so. In fact, I feel it's easier to read and to understand.


Alt Text

Stop Putting Your Thumb On The Scales!

If you haven't figured out by now, one of my pet peeves in tech is when someone tries to advocate for Solution X over Solution Y by presenting rigged or misleading information. With certain lazy audiences, such an approach can help you "win" your argument. But with any discerning listener, you'll end up discrediting your own case.

I can show you examples of royally-effed-up relational databases. And then I could put those examples against carefully-organized NoSQL databases. And to the uninitiated, it may seem that relational databases are Da Sux. And NoSQL databases are Da Bomb. But anyone who truly understands the issue will look at my rigged example and discard my argument.

As a React dev, I used React examples because A) that's a world I'm well familiar with, B) it was a React example in another article that sparked this response, and C) I've seen, first hand, how the perpetuation of these bad examples perpetuates their use in everyday code and skews legitimate debate about future React best practices.

This article isn't about "class-based components are great" or "function-based components are stooopid". In fact, I essentially stopped writing class-based components altogether in favor of functions + Hooks.

But if we're going to compare classes-vs-functions. Or if we're going to compare any solution vs any other solution, at least take the time to gather clean examples. Otherwise, it presents a warped version of the underlying reality.

Discussion

pic
Editor guide
Collapse
webketje profile image
webketje

I would argue that the code styles compared in this article are both equally valid, and that the rest is pure preference. Whether you use .bind in the constructor or define class fields to which you assign arrow functions and then use that as methods depends on:

  • familiarity preference,
  • what stage of ES proposal syntax you want to use and are comfortable preprocessing before it is offically in the spec: in this case github.com/tc39/proposal-class-fields is a Stage 3 proposal (fairly mature).
  • how flexible you'd like your component setup to be: in your example, you cannot, say, map initial props to initial state, as you no longer have access to the constructor(props) param.
  • what you'd like to actually do behind the scenes: if you use arrow syntax for all methods, you lose all re-usability of prototype methods, and every time the component is recreated, all "methods" are redefined on the instance at component instance initialization.

React created the 'binding' problem by first advocating for class components using this everywhere, and then requiring developers to pass functions without their context as props down to child components to achieve certain things. Then all React devs started to advocate for arrow syntax class methods for consistency and because block scope is easier to understand than function scope (arguably, both are true).

However, render() should never be arrow-syntaxed in normal React usage as it is never passed as a prop to other components. In mithriljs, -another JS component framework that uses hyperscript primarily-, the view method MUST be defined without arrow syntax, because it enforces prototype method re-use (see the ES6 class example). But the superior approach is to define the initial state in a closure and reference it by name instead of using this (e.g. see mithril's closure state example). It seems that with hooks, React is finally going where mithril already was 5 years ago, with React again hiding parts of what it does "magically" behind hooks.

Collapse
bytebodger profile image
Adam Nathaniel Davis Author

I think we're mostly in agreement and I appreciate the feedback.

I would argue that the code styles compared in this article are both equally valid, and that the rest is pure preference.

My argument is not that either/any approach is "invalid". My argument is: Don't purposely (lazily) show the most old-skool, verbose example that you know to use for class-based components, and then hold up that particular syntax as an example of why functional components are better. Maybe functional components are better. But if they are, it's not because they're more terse than class-based React components that could've been written in 2015.

It'd be like me trying to tell you that electric cars are superior to gas cars. And to "prove" my point, I show you a comparison of a Tesla vs. a Model T. Maybe electric cars are better. But comparing a Tesla to a Model T does nothing to prove/disprove the point. It's apples-n-oranges.

how flexible you'd like your component setup to be: in your example, you cannot, say, map initial props to initial state, as you no longer have access to the constructor(props) param.

Yes, this is true. But again, if I'm trying to make a "case" that functional components are better, it's kinda lame to throw in all those unnecessary LoC onto my class-based example when those extra LoC don't serve any purpose in the component itself. If someone were using an example that leveraged initializing state with props, then I would totally understand them using the constructor example. But instead, they use this dead-simple example where the state variables are not initialized with props, and then they point to all those extra LoC in the constructor of the class-based component - as though that is a pertinent example in the functional component's favor.

what stage of ES proposal syntax you want to use and are comfortable preprocessing before it is offically in the spec

I've never seen a production deployment of a React app that didn't also leverage Babel. Which makes the whole point of "what's officially in the spec" kinda moot. Granted, every dev shop is perfectly within their rights to decide, for their own purposes, what they're comfortable seeing in their own codebase. But as long as the team's comfortable with Syntax A, and Syntax A can be transpiled through Babel back down to ES2015 (or earlier, if desired), it shouldn't much matter.

if you use arrow syntax for all methods, you lose all re-usability of prototype methods, and every time the component is recreated, all "methods" are redefined on the instance at component instance initialization

I personally find this to be a strong argument in favor of using arrow syntax for all methods. Any time I've personally seen the class methods get reassigned, it's always been a mistake/bug. That being said, I'm sure there are some cases where you could argue for such "flexibility". But it feels like an extreme edge case to me.

React created the 'binding' problem by first advocating for class components using this everywhere, and then requiring developers to pass functions without their context as props down to child components to achieve certain things.

I don't disagree. But given that this has, essentially, been "solved" in React now for quite some time, I just think it's lame to keep trotting these examples out there to illustrate why functional components are The Epic Hotness and class-based components are The Ultimate Fail. Again, I'm not saying that functional components are worse. But if someone's gonna do a tutorial to show why they're better, at least have the intellectual honesty to do an apples-to-apples comparison.

However, render() should never be arrow-syntaxed in normal React usage as it is never passed as a prop to other components.

This is the only part of your reply that I honestly don't understand and/or agree with. But it's not really that big of a deal. I couldn't care less if the render() method is ever "passed as a prop to other components". I'm not seeing even a single piece of lost/broken/changed functionality that results from defining the method as render = () => {...}.

Collapse
webketje profile image
webketje

Hey Adam, thanks for the reply, I take the point of the article (that you further clarified here).

My point of the "render should never be arrow-syntaxed" is: 1) it is highly unusual for React components' render methods to be invoked by anything but React's internal rendering cycle, so the danger of this context mismatch is 0 (except if you do call it directly).
2) JS prototype-attached methods allow devs to define a single method that is the same for, and can be used by any amount of instances, without being redefined (or bound). 3) Defining a class field as method = () => {} will transpile to code inited in the constructor, (this.method = function() {}), and not Comp.prototype.method = function () {}, leading this.method to be re-defined for every component instance.

So say you have 1000 instances of 1 component, that's 1000 function re-definitions where you could've had 1. You won't notice this perf enhancement unless you have a massive amount of component instances that define a massive amount of methods though.

Thread Thread
bytebodger profile image
Adam Nathaniel Davis Author

Ahhh, OK. That does make sense. I hadn't thought of it in that context. But I suppose that, since every component has a render() method (in a class-based component), that could certainly be inefficient (although, as you've noted, it would only present itself in a very sizable app).

Thank you for taking the time to explain that to me!