This isn't your typical piece of Hooks brainwashing, designed to shame you for using class-based components, and extolling the Ever-So-Holy Virtues of Hooks. This isn't even a deep-dive into what Hooks are and why you should (or shouldn't) use them. Instead, this is a case study in how the dogma of The Holy Order of JavaScript has led to an extended exercise in dumping one perfectly-useful paradigm in favor of the new Flavor of the Day - merely because certain "thought leaders" decided to go on a Holy War against JavaScript's class
keyword.
Background
I've been doing professional React development now for about 5 years (and many other flavors of development for more than 20). I don't easily jump onto new technologies. I have too much "real stuff" to accomplish to be chasing every "Package of the Day" that pops up on NPM. So I wasn't a bleeding-edge adopter of React. But when I did finally "see the light", it definitely clicked with me in a significant way.
As a long-time JavaScript dev, I watched (with some amusement) as a certain cadre of the language's "thought leaders" started railing against the unfathomable, unholy, unimaginable horror that is: the JavaScript class
keyword. I read many of the same think pieces that you probably did. I read all of the "justifications" for why it was supposedly-evil - even though it's nothing more than syntactic sugar, that provides absolutely nothing to JavaScript that you couldn't already do.
I can't say that I really cared too much about the circular debates. I thought I was fairly "class-neutral". I saw class
for what it was - simply a keyword. I understood that keywords are neither "good" nor "bad". They just... are. If you want to use that particular keyword, then great! If you don't like that keyword, that's great, too! You do you!
Meanwhile, as a React dev, I couldn't really escape class
. Before 2018, if you were cranking out React code, you were mostly doing it with class-based components.
Sure, there's always been a conceptual focus on pure functions. But pure functions have no life cycle. They have no state. They're... functions. And if you're building any kinda sizable app, at some point, you're gonna have to reach for those life-cycle methods and those state-management capabilities.
The Cost of Dogma
Every flavor of tech has its dictators. The snobby types who will tell you that your code sucks if you ever make the mistake of using an old-skool function()
declaration instead of an oh-so-cool arrow function. They try to code-shame you because you don't leave an empty line above your return
statements. Or because you don't put the opening {
on its own line.
For the most part... I just ignore these dictators. I have deadlines. I have paying clients. I can't be bothered to refactor a 100k+ LoC application because some hot new blog post says that there should be no inline styles. Write your snooty blog posts. High-five your dictator friends. I have work to do.
But that changed in 2018. In October of that year we were blessed from on-high with... Hooks.
And the fanboys rejoiced.
When Hooks were introduced, I worked with an extremely talented React dev and he was almost beside himself. He was gleeful. And I was... happy(?) for him. But he kept showing me these Hooks examples and praising them as so obviously-superior. And I kept looking at the examples and thinking, "Yeah... it's just another way to do all the stuff we could already do - in classes."
You see, it's fine to tell all your buddies that tabs are wayyyyyy superior to spaces. But when you have the clout for your new package to be included right alongside React's core build - and your new package attempts to enforce tabs, or your "community" attempts to code shame people into using tabs, well then... you're just kinda being a jerk.
Lessons From Marketing
Not that any of this really bothered me much at the time. I still had thousands of lines of code to work on - code that was humming along perfectly in class-based components. No one was gonna pay me to rewrite all of their perfectly bug-free class-based components.
If you've ever taken a Marketing 101 course, you learn that people need a compelling reason to change products. Just telling me that you have a new toothpaste won't compel me to switch to it. Or even try it. I already have a prefered brand of toothpaste. It works great. It tastes great.
If you want me to switch to your new brand of toothpaste, you'll have to come up with something better than "It's new!" or "It's different!" You'll have to give me a compelling reason to change.
Sadly, these "market forces" tend to get perverted in the programming community. Joe comes up with a New Way to write JavaScript. He yells to all of his buddies that they should code in the New Way. And... everyone just shrugs.
But what if Joe is seen as a "thought leader"? What if he's already hailed by the fanboys as a programming "legend"?? Well... in that case, the fanboys start lining up behind him. Not only do the fanboys start shifting all of their coding to mirror Joe the Thought Leader, but they also start code-shaming you if you don't fall inline.
Don't believe me? Consider a comment that was placed on one of my other posts that had nothing to do with the class-vs-function debate:
I will never understand how anyone can prefer class over function component.
That's it. No intelligent discourse on the content of the post. No meaningful feedback at all. Just a trolling non sequitur because my stooopid code examples used... classes.
The fanboys aren't content with simple trolling. They're also happy to peddle in Grade-A #FakeNews. In numerous examples all over the web, I've seen almost this exact same comment left on React blog posts:
Class-based components are being phased out.
Umm... no. They're not. If you don't believe me, just spend a few minutes reading the Hooks documentation on the core React site. It's clear. It's unambiguous. It states that, "There are no plans to remove classes from React." Apparently, the fanboys are unwilling (or unable) to read this basic unequivocal statement direct from the React team.
Comparisons
For the last couple of years, I've been mostly silent about Hooks. I don't hate them. I don't love them. I just see them as... a thing. A tool that can prove useful in certain circumstances - and not-so-useful in others. Like almost any React dev, I've tinkered with them in my local environment. But for the most part, they've been a side note. This happened because my employers - the people who actually, you know, pay me to write code, still have huge legacy codebases that are filled with classes. And it's not exactly straight-forward to start converting all that stuff to Hooks.
The last couple months have been a big eye-opener for me. I joined a new company and we have the privilege of doing some "green fields" development. Before we wrote anything, we all had a discussion about tools, techniques, and best practices for the new project. And we decided as a group that all this new code would be done with pure functions and function-based components (i.e., with Hooks). So I've finally had the chance to do a true "deep dive" on Hooks.
Not only have we been cranking out brand new code with Hooks, but I really wanted to get up-to-speed quickly on them. I have a large side-project that currently sits at more than 30k LoC, and I've taken it upon myself to convert all of that to Hooks. After spending several hundred hours deeply immersed in all-things-Hooks, I can confidently say that my assessment is...
Meh...
Before you start rolling your eyes, please understand that I have nothing in particular against Hooks. They're fine. They're great. But when you've converted a few hundred class-based components into Hooks, after awhile it's amazing how much the new, oh-so-cool, function-based components look like... class-based components.
First, let's look at a dead-simple example:
// the old, evil, class-based component
export default class CancelButton extends React.Component {
render() {
return (
<Button
onClick={this.props.onClick}
style={{
backgroundColor : the.color.cancel,
color : the.color.white.text,
...this.props.buttonStyle,
}}
variant={the.variant.raised}
>
<FontAwesome
name={the.icon.x}
style={{marginRight : 10}}
/>
<TranslatedTextSpan english={'Cancel'}/>
</Button>
);
}
}
Is now this:
// the amazing, fantabulous, function-based component
export default function CancelButton(props) {
return (
<Button
onClick={props.onClick}
style={{
backgroundColor : val.colors.lightGrey,
color : val.colors.nearWhite,
...props.buttonStyle,
}}
variant={'contained'}
>
<FontAwesome
name={val.icons.x}
style={{marginRight : 10}}
/>
<TranslatedTextSpan english={'Cancel'}/>
</Button>
);
};
Wow... what a difference! The function-based component is just sooooo much better, right???
Umm...
OK, to be fair, maybe that example is just too simple to illustrate the many benefits of function-based components. After all, it doesn't even have any Hooks in it. So let's look at something a bit juicier:
// the old, evil, class-based component
export default class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
fields : {emailAddress : I.getDefaultFieldProperties()},
okButtonIsDisabled : true,
};
}
checkForEnter(event) {
if (!this.state.okButtonIsDisabled && event.keyCode === the.keyCode.enter) { this.callCreateLogIn(); }
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.updateOkButtonState();
}
dismissAlertAndGoHome() {
app.DisplayLayer.dismissAlert();
app.DisplayLayer.updateModule(<HomeModule />);
}
goToRegister() {
app.DisplayLayer.updateModule(<RegisterModule />);
}
render() {
const {fields, okButtonIsDisabled} = this.state;
return (
<FullHeightPaper>
{/* render ALL THE THINGS */}
</FullHeightPaper>
);
}
updateFieldState(event) {
const updatedFieldState = I.getUpdatedFieldState(event.target, this.state);
this.setState(updatedFieldState);
}
updateOkButtonState() {
const {fields, okButtonIsDisabled} = this.state;
if (this.logInFormIsInFlight) { return; }
const someFieldsAreInvalid = Object.keys(fields).some(fieldName => fields[fieldName].isValid === false);
if (someFieldsAreInvalid !== okButtonIsDisabled) { this.setState({okButtonIsDisabled : someFieldsAreInvalid}); }
}
}
Is now this:
// the amazing, fantabulous, function-based component
export default function LoginForm() {
const displayLayer = useContext(DisplayLayerContext);
const model = useContext(ModelsContext);
const sessionApi = useContext(SessionApiContext);
const [emailAddressField, setEmailAddressField] = useState(model.textField());
const [okButtonIsDisabled, setOkButtonIsDisabled] = useState(true);
const checkForEnter = (event = {}) => {
if (!is.aPopulatedObject(event))
return;
if (!okButtonIsDisabled && event.keyCode === val.keyCodes.enter)
sendLogIn();
};
const goToHome = () => {
displayLayer.updateModule('home');
};
const goToRegister = () => displayLayer.updateModule('register');
const handleErrors = (errors = []) => {
if (!is.aPopulatedArray(errors))
return;
if (errors.find(responseError => responseError === 'email does not exist')) {
let alert = model.alert();
alert.icon = 'warning';
alert.text = translate('The email address supplied could not be found in our records.');
alert.title = translate('Oops!');
createAlert(alert);
} else {
displayLayer.createGenericErrorAlert();
}
setEmailAddressField(model.textField());
};
const updateFieldState = (event = {}) => {
if (!is.aPopulatedObject(event))
return;
let clonedEmailAddressField = cloneObject(emailAddressField);
clonedEmailAddressField.value = event.currentTarget.value.trim();
clonedEmailAddressField.isValid = isEmailAddressValid(event.currentTarget);
setEmailAddressField(clonedEmailAddressField);
setOkButtonIsDisabled(!clonedEmailAddressField.isValid);
};
return (
<FullHeightPaper>
{/* render ALL THE THINGS*/}
</FullHeightPaper>
);
};
OK, this is a much more "involved" example. We're using useState()
! And we're using useContext()
! And the function-based component is a clear, hands-down "winner" over the class-based component... right???
Umm...
If you're not instantly recognizing the clear and obvious superiority of my function-based component over my old, ugly, nasty, class-based component... then congratulate yourself. You're not a mindless fanboy who's singing the Hooks praises merely because one of React's main contributors told you to.
Real-World Code
I've seen soooo many lame examples on the web where someone converts an old, ugly, class-based component into some (supposedly) beautiful function-based component and then uses it to sing the praises of Hooks. The problem with these examples is that they are rarely reflective of real, live, out-in-the-wild code.
To be perfectly clear, I can absolutely find some examples where my function-based component ended up somewhat smaller and nominally "cleaner" than the original class-based example. Unfortunately, I've found these examples to be relatively rare.
When you really start diving into Hooks, the reasons for the near-one-to-one conversion become clear:
Hooks are just another way to do what you could already do in class-based components.
State's messy. But you can rarely avoid state management altogether. So when you start porting all that state management from class-based components over into Hooks, it looks shockingly similar.
Life cycle's messy. But you can rarely avoid life-cycle management altogether. So when you start porting all that life-cycle management from class-based components over into Hooks, it looks shockingly similar.
And I haven't even shown any of my conversions that use useEffect()
and useCallback()
. When you start getting into that level of detail, it's not uncommon for the class-based components to look downright simpler.
The End-Result of Dogma
Let me tell you exactly how we got to Hooks. About 5 years ago, a certain segment of the JavaScript Illuminati decided that:
Classes are bad... mmmkay???
When they did, this presented a quandary for the React community. React was already well down the class
road. And even though the React community started to yell ever-louder about the horrible, unsightly, ugliness of that unconscionable class
keyword, there was always a central problem: You couldn't do a lot of "React stuff" in pure functions. Specifically, you couldn't do some of the key features like state and life-cycle management.
The whole class
hatred might've died right there, except... The Redux team was totally onboard with the "classes must go" mantra. So they created Hooks. Then they used their considerable clout in the community to make it clear that Hooks absolutely are The Next Big Thing.
And then... the fanboys mindlessly fell inline.
So now, if you're trying to write a React blog post, or demonstrate in an interview, some concept that has nothing to do with the classes-vs-functions debate, you must be wary of any potential Class Haters lurking in the audience. Because if you throw one of those evil class
keywords on the whiteboard, that might literally be the end of the discussion for them.
Drop the Hatred
You might think that I'm some hardcore Hooks hater. But nothing could be further from the truth. The simple fact is that Hooks are a tool in your tool belt. Your hammer isn't "good" or "bad". It's "good" in certain situations. And downright pointless in others. The same can be said about Hooks. Or classes.
Code-shaming someone because they use classes makes as much sense as vilifying a craftsman because he uses a hammer.
I've actually enjoyed a lot of my recent Hooks development. They have some clear advantages (that I'll highlight in a future post). I've also found that they definitely have some... challenges. Challenges that I didn't have to deal with in class-based components.
The key isn't to decide whether Hooks are "bad" and classes are "good" (or vice versa). The key is to understand what Hooks and classes are: syntax.