Stahhp With The Outdated React Techniques!
Adam Nathaniel Davis
Posted on August 20, 2020
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.
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:
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:
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.
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.
Class functions declared in this way cannot be accidentally redefined at runtime.
Removing bindings from the constructor clears the way for us to remove other things from the constructor...
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:
Initializing state variables
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.
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.
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.
Posted on August 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.