As part of the ES2015 release, classes were formally introduced to native JavaScript as syntactic sugar for prototypical inheritance. Object oriented developers everywhere popped champagne and celebrated in the streets. I was not one of those developers.
π The Web Landscape
Coincidentally, this was also the time that the JavaScript community was being introduced to React. A library that unabashedly pushed its way past existing modular library seemingly overnight. React's creators took lessons learned from Angular 1.x, introduced jsx, and taught us it was OK to JS all the thingsβ’οΈ. Got JS? Html? Css? Leftover π? Throw it all in there, it'll blend.
π Stay Classy
Classes provided a nice cork board for React to pin their patterns to. What is the recipe for a React class component you ask?
- Create a new file
- Write a class that extends
React.Component
- Repeat
Not much to it. Easy peasy one two threezy. This pattern really flattened the curve for developers learning React. Especially those coming from object oriented languages.
Everyone take a moment and wave π hi to their old friend Readability. As with any new framework, adoption is strongly coupled to readability. React's high readability resulted in most code samples being comprised of classes. Hello world, todo app tutorials, learning resources, Stack Overflow, coding videos; classes as far as the eye can see.
π€·ββοΈ So What's the Problem
For the most part, everything was peachy in the beginning. We had well-defined class components. We had modular, testable pieces of functionality. Life was good. However we know all good things must come to an end. As your React project's codebase grows you realize you're having to write a fair amount of boilerplate.
import React from 'react';
const MIN_POWER_TO_TIME_TRAVEL = 1.21;
const MIN_SPEED_TO_TIME_TRAVEL = 88;
class DeLorean extends React.Component {
constructor() {
super();
this.state = { gigawatts: 0 };
}
static const MIN_POWER_TO_TIME_TRAVEL = 1.21;
static const MIN_SPEED_TO_TIME_TRAVEL = 88;
componentDidUpdate() {
const { isLightingStriking } = this.props;
if (isLightingStriking) {
this.setState({ gigawatts: DeLorean.MIN_POWER_TO_TIME_TRAVEL });
} else {
this.setState({ gigawatts: 0 });
}
}
hasEnoughPower(gigawatts) {
return gigawatts >= DeLorean.MIN_POWER_TO_TIME_TRAVEL;
}
hasEnoughSpeed(mph) {
return mph >= DeLorean.MIN_SPEED_TO_TIME_TRAVEL;
}
render() {
const canTimeTravel =
this.hasEnoughPower(this.state.gigawatts) &&
this.hasEnoughSpeed(this.props.mph);
if (!canTimeTravel) return <span>π</span>;
return (
<div title="Great Scott!">
<span>π₯</span>
<span>
{gigawatts} GW / {mph} mph
</span>
<span>π</span>
<span>π₯</span>
</div>
);
}
}
NOTE: I am fully aware that this component's implementation is not perfect, but it is typical.
Do you see the class ... extends React
, constructor
, super()
, render()
lines? These will be needed in every class component you write. My wrists hurt thinking about all the redundant keystrokes. If you don't think lines of code are important, try wrapping your head around a 1000+ line component file. Es no bueno π.
Inevitably you will find yourself debugging your new shiny component because it explodes for one reason or another.
- Did you forgot to add the
constructor
method? - Did you call
super()
? - Should you be using some other lifecycle method?
componentDidMount
componentWillMount
componentRedundantPrefixMethod
- ...or other undocumented/unstable method?
- How are you going to test the
hasEnoughPower
andhasEnoughSpeed
methods? - Wtf is
static
? - Oh no, not "this" again
I realize these all are not necessarily issues with classes, but our React class components aren't as perfect as we first thought.
π£ Enter Hooks
If we fast forward a few minor versions of React we get a shiny new feature called hooks
. One of the key benefits of hooks is that they allow us to leverage all of the component lifecycle methods in functional components. No weird syntax or boilerplate code required.
Here's the hook-ified version of our stainless steel class component...
import React, { useEffect, useState } from 'react';
const MIN_POWER_TO_TIME_TRAVEL = 1.21;
const MIN_SPEED_TO_TIME_TRAVEL = 88;
const hasEnoughPower = (gigawatts) => gigawatts >= MIN_POWER_TO_TIME_TRAVEL;
const hasEnoughSpeed = (mph) => mph >= MIN_SPEED_TO_TIME_TRAVEL;
const DeLorean = ({ isLightingStriking, mph }) => {
const [gigawatts, setGigawatts] = useState(0);
useEffect(() => {
if (isLightningStriking) {
setGigawatts(MIN_POWER_TO_TIME_TRAVEL);
} else {
setGigawatts(0);
}
}, [isLightingStriking]);
const canTimeTravel = hasEnoughPower(gigawatts) && hasEnoughSpeed(mph);
if (!canTimeTravel) return <span>π</span>;
return (
<div title="Great Scott!">
<span>π₯</span>
<span>
{gigawatts} GW / {mph} mph
</span>
<span>π</span>
<span>π₯</span>
</div>
);
};
There's a lot going on here, especially if you haven't used hooks before. I suggest you take a few minutes to skim through React's hook documentation to get familiar if you aren't already.
The key takeaways are:
- We can export and test
hasEnoughPower
andhasEnoughSpeed
methods without adding boilerplateΒΉ - We reduced our total lines of code by ~10 (25% less)
- No more
this
keyword - Boilerplate, "I-only-put-this-in-because-it-won't-work-without-it" code is completely removed
- We're back to using functional composition in a functional language
- Functional components are smaller, more so when minified
ΒΉ I know we could have exported those two methods in the class example, but in my experience this is how I've seen the majority of components implemented. Where everything is a class method and accessed by this
π What If I Am Using Typescript?
WARNING: Strong opinions lie ahead...
This post is about increasing readability and writing less code with better test coverage by specifically avoiding the use of classes.
My current opinion of Typescript is that it increases lines of code, reduces velocity, and fully embraces inheritance. It forces OOP patterns into a functional language in exchange for type checking. Hold on, I have to go write some typings... Where was I? Oh yeah, getting lost in context switching π.
If you are stuck writing Typescript I'm sorry and I feel for you. I've been there and it was not enjoyable (for me). Stop reading this post as it might tap into the well of stress and development frustration you have tried so hard to ignore.
Now back to our regularly scheduled post...
π Exceptions to Every Rule
As of writing, there are still a few places that classes are a necessary evil. These are considered very niche and make up a very small subset of use cases in most projects.
- When extending
Error
into custom errors - When using React's
Suspense
, classes useful for capturing errors in error boundaries
π Where Does this Leave Us?
I hope/speculate that classes will eventually be exiled to outer reaches of the JS community, a la generators
. Neat to show off in academia with very few real world use cases.
React is already migrating that way. Don't take my word for it, take a look at their documentation. Their examples are mostly functional components with footnotes for class versions. They've even posted a formal statement that they prefer composition over inheritance (read: functions over classes).
Disagree? Love classes? Spot on? Let me know in the comments below.
Today's post was brought to you by VSCode's "duplicate line(s) above/below" shortcut: Shift+Option+(UpArrow|DownArrow)
Top comments (0)