A Hooks-vs-Classes Report Card
Adam Nathaniel Davis
Posted on April 11, 2020
Hooks have been out now for a year-and-a-half. Maybe I'm late to the game, but I've only been using them heavily in the last few months. Part of my hesitance was in some of the blatant misinformation that I read on many blogs (and even... in the official docs).
At this point, I don't claim to be any kind of "Hooks expert". I'll freely admit that there's much about the pattern that I have yet to fully grok. But in short order, I can confidently claim that I've been writing many thousands of LoC - both in brand-spankin-new Hooks-based components, and in old class-based components that I've converted to Hooks. So at this point, I'm thinking that some of you might benefit from the conclusions that I've recently come to (and some that I'm still slinging around in my head).
I'm no "class hater". I don't subscribe to any of the silly dictates that many JavaScript devs use to dismiss class-based components out-of-hand. If you look through any of the other articles in this series, that basic fact will be obvious to you.
But I'm not interested in being a "Hooks hater" either. One of my common refrains is that all of these programming constructs are tools. And dismissing Hooks (or classes) because of some mindless dogma that you read on some "thought leader's" blog post is just as silly as throwing out your hammer (or shaming other people for using a hammer) just because you've decided that every job should be accomplished solely with a screwdriver.
So without further ado, I'm gonna try to compare some of the major advantages that others claim to see in Hooks versus class-based components (and vice versa).
If you're already firmly in the camp of the "Hooks fanboys" or the "class fanboys", I have no doubt that you're gonna disagree - vehemently - with some of my conclusions. That's OK. You won't be the first person to think I'm an idiot - and you won't be the last.
Code Size
Classes: B-
Hooks: B+
One of the things that inspired me to write this post is the fact that sooooo many of the Functional-Programming Evangelists seem to talk about functions (and Hooks-based components) as though they are - hands-down - a faster, cleaner, more-efficient way to write code. After putting in about 30k LoC in Hooks-based development, I gotta tell ya that... I'm just not seeing it.
When converting class-based components to Hooks, I've noticed that, sometimes, the Hooks-based equivalent comes out being a little shorter. But it's hardly a clear win for Hooks.
Even worse, in many of the "how to use Hooks" tutorials I've seen, they use some kinda loaded example where they seem to purposely write the class-based component in a sloppy, verbose manner. Then they convert it to some kinda slimmer version in Hooks and they pat themselves on the back about the supposedly-obvious improvement.
For example, they often show code snippets like this:
// the evil class-based component
export default class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {counter: 0};
this.increment = this.increment.bind(this);
}
increment {
this.setState((prevState) => {
return {counter: prevState.counter + 1};
});
}
render {
return (
<>
<div>The counter is: {this.state.counter}</div>
<button onClick={this.increment}>Increment</button>
</>
);
}
}
// the oh-so-superior Hooks-based component
export default function Foo() {
const [counter, setCounter] = useState(0);
return (
<>
<div>The counter is: {counter}</div>
<button onClick={() => setCounter(counter + 1)}>Increment</button>
</>
);
}
Ehr-mah-gerd! The Hooks-based component is such a clear advancement over that mean, old, ugly class-based component!!!
And that comparison makes perfect sense - if you haven't written a line of React code since 2014.
Of course, there's no reason to write that bloated class-based component shown above. We don't have to bind functions. We don't have to use constructors. We don't even have to use a standalone update function. Instead, it's perfectly valid to write the class-based component like this:
export default class Foo extends React.Component {
state = {counter: 0};
render = () => {
return (
<>
<div>The counter is: {this.state.counter}</div>
<button
onClick={() => this.setState(state => ({counter: state.counter + 1}))}
>Increment</button>
</>
);
};
}
The Hooks-based component is a little smaller. And I'd be the first to admit that the inline call to this.setState()
in the streamlined version of the class is... a bit unwieldy.
But the point is that it's far from a clear-cut, hands-down victory for Hooks. In classes, you can't avoid defining a separate render()
function (which adds two whole lines of code!!!). And class-based components, even in the best scenario are a little bit longer on average. But the rhetoric around Hooks being soooo much shorter/cleaner/prettier is just way overblown.
Here's another silly little "cheat" that I see in some of these online examples:
// the evil class-based component
export default class Foo extends React.Component {
state = {counter: 0};
doSomething = () => {
// all the hairy doSomething() logic
}
doAnotherThing = () => {
// all the hairy doAnotherThing() logic
}
doSomethingElse = () => {
// all the hairy doSomethingElse() logic
}
render = () => {
return <div>The counter is: {this.state.counter}</div>;
};
}
// the oh-so-superior Hooks-based component
const doSomething = () => {
// all the hairy doSomething() logic
}
const doAnotherThing = () => {
// all the hairy doAnotherThing() logic
}
const doSomethingElse = () => {
// all the hairy doSomethingElse() logic
}
export default function Foo() {
const [counter, setCounter] = useState(0);
return <div>The counter is: {counter}</div>;
}
Ehr-mah-gerd! The Hooks-based component is soooo tiny. That's awesome!!
Umm... yeah. It's only "tiny" because you've exported all the necessary supporting functions outside the component. And BTW... you can do the exact same thing with class-based components.
If you think this is a silly example, I assure you that I've seen very similar examples where someone is trying to "prove" the superiority of Hooks. I'm not going to blog-shame anyone by linking to them here. But I'm sure you can find them if you look hard enough.
Shared-State Management
Classes: B+ (C)
Hooks: A-
My similar grades might confuse some people. I've heard a lot of chatter about the supposedly-epic ability of Hooks to share state. But what I've noticed is that those people rarely make a distinction between sharing stateful logic, and simply sharing state.
The Hooks documentation itself is very clear on this point. It states:
Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
IMHO, there's a lot of confusion out there on this point. People talk about shared state in Hooks like you just need to spin up a useMagicSharedState()
Hook - and it works like Redux, or MobX, or any other third-party state-management solution.
So why do I give Hooks a lofty "A-" on this point? For two reasons:
Hooks have a much cleaner, far-more-intuitive way to use context. This is especially true when you're trying to use multiple contexts in a single component. With Hooks, you just throw out that
useContext()
Hook, for as many contexts as you need to consume, and it just basically... works.Hooks do have some amazing capabilities to create true, global, shared state. You can do this with custom Hooks - but it's not immediately evident how to make it work from their documentation. If you want the details on that, you can check it out here: https://dev.to/bytebodger/hacking-react-hooks-shared-global-state-553b
To be frank, class-based components aren't really that far behind. I give them a "B+" because the new(ish) Context API is, IMHO, extremely powerful and vastly underused in most modern React applications. It's not nearly as cut-and-paste as Hooks, but I explained one way to fully leverage these capabilities in this post: https://dev.to/bytebodger/a-context-api-framework-for-react-state-management-1m8a
However, I give class-based components a parenthetical "C" in this category because most dev teams aren't using, or are barely using the Context API. And they're usually afraid to pass state through props if it involves more than one or two layers.
This means that most class-based React applications are highly convoluted by additional state management tools. I've actually come to the conclusion that nearly all state-management libraries are a code smell. But I have a particular distaste for Redux. It's no accident that Redux rhymes with sucks...
[Editor's Note: At this point, Adam went off on a 10,000-word diatribe about his deep-seated hatred for Redux. If you've read anything else from him before, this is already "old news" to you. I cut out all of the Redux Sucks Tome for easier reading. You're welcome...]
Legacy Compatibility
Classes: A+
Hooks: C+
OK, maybe this is an unfair comparison. Unless you want to write all your components with React.createComponent()
, classes are legacy. So of course they're "legacy compatible".
But Hooks deserves at least some criticism for the fact that they don't always easily integrate with class-based components. Yes... I know that the Hooks documentation touts them as being perfectly backwards compatible. And they take great pains to state you can build Hooks-based components right alongside your old, smelly, class-based components.
The problem I've found is mainly in dealing with third-party (i.e., NPM) packages. Whenever I'm considering using a new package nowadays (or when I'm considering upgrading an existing package), I have to look carefully at the documentation to see how I'm expected to implement it.
A Hooks-based component is still just a component. So if I need to import
that component and then plop it in the middle of a render()
, that tends to work just fine. But I've noticed a disconcerting number of packages where they require me to leverage the Hooks directly - not just the Hooks-based components. And when you do that... your console starts throwing all those errors that happen whenever you try to leverage a Hook directly from within a class.
Lifecycle Management
Classes: B-
Hooks: F
Maybe you're thinking that this is also an unfair comparison. After all, Hooks-based components are all functions. And functions have no "lifecycle". You just call them and... they run.
But let's get real here. When you're writing Hooks-based components, you may be using a function
keyword. Or you may be using the arrow syntax. But under the covers, that component's not really running like a "true" function.
Every React component is ultimately a slave to the virtual DOM. In theory, the virtual DOM is the "secret sauce" that makes React do all those cool, nifty things without you having to manually program all of the event handlers to make them work. But this ultimately means that you never have full control of a component. It will always be beholden to the rendering cycle that's central to React's magic.
But that rendering cycle means that your "function" is going to be called, repeatedly, without you ever having manually triggered it. This means that, whether you want to admit it or not, all React components have an inherent lifecycle. And yes, that includes Hooks-based components.
Writing Hooks-based components can be downright simple and pleasurable - for a large portion of your codebase. But if your app is doing anything more than cranking out "Hello World!" messages, at some point, you will find yourself fretting over component lifecycles.
This is where I find Hooks to be borderline-hideous. You start reading (and re-reading) all the Hooks documentation for "the Hooks equivalent of lifecycleMethodX()". And then you start realizing that, for many of those lifecycle methods, the Hooks equivalents are... clunky. In the worst scenarios, they simply don't exist at all.
This isn't to imply that the "normal" lifecycle methods in class-based components are "fun" or "easy". In any sizable application, lifecycle management is basically a necessary evil. It can be frustrating. It can be a source of nasty bugs. But it is necessary.
Hooks attempt to address most of this with useEffect()
. Yeah... good luck with that. Pretty soon, you have too many effects, and your dependency array is starting to scroll off the right side of your IDE. And once you start reaching for useCallback()
...??? Oh, vey.
In class-based components, I've rarely ever had a problem with infinite renders. Since I've been diving into Hooks, I've already lost track of how many times I've accidentally spawned the Infinite Render Beast while I'm trying to code (what seems to me like) a simple bit of "calculate X, then render Y" logic, or "render X, then do Thing Y".
Developer Cognition
Classes: C
Hooks: C
No one is going to agree with both of those grades. I give them both a "C" because I've (finally) learned that your perception of Hooks-vs-classes probably says a lot more about your background as a developer than it does about any putative benefits of the tools themselves.
Did you first learn to code in the last half-decade or so? Do you only code in JavaScript? (And I'm not implying that there's anything "wrong" with that, if you do.) Did you get your first programming experience at a code camp?
If you answered "yes" to any of those questions, there's a strong possibility that Hooks "feel" more logical to you. It took me quite a while to finally grasp this reality, but the latest generation of frontend/JS-only (or JS-centric) devs just seems to have some kind of mental block when it comes to that nasty, scary, dirty class
keyword. For them, class
is the algorithmic equivalent of "moist".
If you're more like me: If you've got a little grey in your beard. (Who am I kidding? My beard's almost totally grey.) If JavaScript is just one of a library of languages in which you're comfortable. If you've seen the good, the bad, and the ugly of object-oriented programming. If you're perfectly comfortable writing your own SQL queries. Or if you've ever had to fret over memory management in an application.
If you're in that category, there's a decent chance that either: A) You're perfectly fine with the class
keyword for what it is - syntactic sugar. Or, B) you don't love JavaScript's "faux classes" - but you've learned to accept and master them as just another tool in your toolbelt.
A practical example of this dichotomy lies in the this
keyword. If you've been doing JavaScript for long enough, you have some war stories about the horrible ambiguities that can arise from this
. But ever since the introduction of const
and let
, I can't honestly remember the last time that I had to track down (or the last time that I created) some maddening bug caused by the ambiguities of this
.
But a recent comment on one of my other posts made me realize that this isn't the case for all JavaScript devs. Some of them are literally confused by the mere presence of this
.
To me, it's dead simple. this
just refers to... this component. More specifically, this
refers back to the class in which this code is written. I don't honestly understand what's so confusing about that - but I now realize that, for some devs, it absolutely is confusing.
Adherence to "Standards and Conventions"
Classes: B+
Hooks: C
Oh, boy. If you're in deep romantic love with JavaScript, and functional programming, and you're still reading, then you're probably having a coronary with this grade.
How can you possibly claim that JavaScript classes are more standards-compliant than functions??? JavaScript is a functional programming language!!!
First, calm down for a minute. Take a walk around the block. Monitor your heart rate. Have a beer (or three). It'll be OK...
At some point in the future I'm gonna crank out a (too) long, annoying post about the silly way that some JavaScript devs have glommed onto the whole catchphrase of "functional programming". But I'm gonna put that one on a shelf for a while...
Let's look at this from the perspective of some really old, really solid programming wisdom. The first is an incredibly simple concept that served me incredibly well when I was a younger developer - and it still continues to serve me well every single day that I'm writing code:
A function should do one thing, and do it well.
That bromide's been around for decades, but it's lost none of its potency. Why do we strive so hard to keep our functions short??? Because, once your function starts getting too long, it's almost certain that it's no longer doing one thing and doing it well. If it was really doing one thing, it would probably be shorter.
Longer functions are almost certainly trying to do too many things. This makes them prone to bugs. It makes it hard to write unit tests for them. It makes it hard for other developers to come behind you and simply read your code to understand what it's doing. Whenever you're refactoring code, it's almost always a good idea to break a longer function into smaller, more-targeted pieces.
So let's look at a typical component:
export default function User(props) {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [age, setAge] = useState('');
const alertUser = (values) => {
// create an alert to let the user know that something went wrong
}
const checkLogin = (values) => {
// ensure that the user's logged in and should be seeing this data
}
const updatePermission = (values) => {
// adjust the user's permissions based on some events fired from the return()
}
const updateUser = (values) => {
// do a whole bunch of update functionality here
}
return <div>...display a whole bunch of user data here...</div>;
}
Maybe this component shell looks pretty logical to you. We have a series of functions, and depending upon what logic we write inside those functions, it's perfectly feasible that each one is doing one thing, and doing it well.
But the functional programming fanboys tend to completely gloss over one key fact:
The entire component is itself... a function.
This means that we have one function that purports to:
- Keep track of multiple state values.
- Display dynamic data relevant to the user.
- Alert the user to problems.
- Check the user's login credentials.
- Update the user's permissions.
- Update the user's core data.
Wow...
The User
function, in this case, is definitely not doing "one thing" and "doing it well". The User
function is responsible for a wide array of functionality.
I can almost hear some of you thinking:
Well... that's why I wouldn't put all of that functionality in this function. I'd put those functions outside the component.
Alright... fine. I already covered above how this really does nothing to make your code "cleaner". It just flings your functionality off into separate functions (which may actually reside in separate files, or even in far-flung directories). But let's just assume for a minute that your component would have all that functionality housed in standalone functions, and that this is, in fact, "better".
Well, then let's consider another well-worn (and still valuable) chestnut of programming. It's called the single responsibility principle. It states:
Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
So now you're yelling, "And that is why I don't use class
!"
The problem is that you can use functions to define all the features needed in your components. But the fact is that your components are far more analogous to classes than they are to functions. Just because you avoided that nasty-ol' class
keyword and you only used "functions", doesn't alter the fact that your components are really working, algorithmically, as classes.
Don't believe me? Scroll back up and read that definition of the single responsibility principle again. But this time, substitute "class" with "component". Hmmm... that starts to sound a lot like the principles for writing a good component.
Components are pretty cool concepts in React. A component can:
- Maintain its own memory.
- Render its own display.
- Handle any number of events triggered from actions that are spawned in that component.
- Be cloned.
- Represent different states (which ultimately spawn different behavior) based on initial-or-realtime input.
Now go find some programming friends who aren't JavaScript-only developers. Rattle off that list of features and ask them what they'd call that "thing". And then count how many of them say, "I'd call that thing... a function."
React's function-based components don't even sound like functions. Functions - in almost any other context - have a very standard naming convention. They're named after actions. Specifically, it's usually best-practice to name your functions with a verb and a subject, like this:
getUser()
translateText()
validateInput()
callApi()
deleteForm()
filterSearchResults()
There's not an experienced programmer in the world who's gonna look at function names like these and have any problems with them. They're clear. They're descriptive. And most importantly, they give you an obvious indication of the one thing that the function's designed to do.
Now let's look at typical names for a function-based component:
<AllFormsPage>
<Row>
<TextField>
<UserModule>
<LeftNavigation>
<LoginForm>
Do any of those component names sound "bad" to you? Because they sound just fine to me. But do any of those component names sound like functions to you?? Because they definitely don't sound like functions to me.
The Verdict
There really is no verdict here. I've tried to highlight how Hooks-based components can be good. And how they can be... suboptimal. Similarly, class-based components can be good. And they can also be... suboptimal.
I've been writing a ton of Hooks-based components lately. And you know what?? I can honestly say that I like them. I'm going to continue writing more of them. They have definite shortcomings. But some of their positives are very alluring.
I have no doubt that you probably have very strong feelings for-or-against Hooks, and for-or-against classes...
Posted on April 11, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.