Before ranting about Hooks, here's a brief review of React's history...
In React, you can just use a plain function as a component. These are great for simple components that don't have any internal state.
function SimpleButton (props) {
return (
<button onClick={props.onClick}>
{props.label}
</button>
)
}
If your component needs to have state, then you would create a "class component".
As an example, let's create a component that tracks: the window width, and how many times we've clicked a button.
We'll start with React version 14, using ES5 syntax. Very approachable, and (aside from JSX) non-magical.
var React = require("react"); // v0.14.8
var ResizableComponent = React.createClass({
// Initialize the internal state of the component
getInitialState: function() {
return {
count: 0, // Clicks of the button
width: window.innerWidth,
};
},
// Use life-cycle methods to setup and teardown an event listener
componentDidMount: function() {
window.addEventListener("resize", this.handleResize);
},
componentWillUnmount: function() {
window.removeEventListener("resize", this.handleResize);
},
// Create two handlers for changes to window size and button clicks
handleResize: function() {
this.setState({ width: window.innerWidth });
},
handleClick: function() {
this.setState({ count: this.state.count + 1 });
},
// Now render out the HTML using the (pretty good) JSX syntax
render: function() {
var width = this.state.width;
var count = this.state.count;
var name = this.props.name; // The props are passed from a parent component
return (
<div>
<p>Hi {name}, the window width is currently {width}</p>
<button onClick={this.handleClick}>Clicked {count} Times</button>
</div>
);
}
});
Then the React community started to move towards ES6 classes. This was (in general) not too bad, but it came with some costs:
- A transpiler became an (almost) "must have" for React work
- Methods are no longer automatically bound to
this
- The first line of your constructor must call
super()
var React = require("react"); // v0.14.8
// or use the import syntax: `import React from "react"`
class ResizableComponent extends React.Component {
// Initial state is in the constructor or with a class-level assignment...
//
// state = {count: 0}
//
// ...but only if using a transpiler like Babel.
constructor () {
super()
this.state = {
count: 0,
width: window.innerWidth,
};
// With ES6 class syntax, you MUST manually bind methods to `this`
this.handleResize = this.handleResize.bind(this)
this.handleClick = this.handleClick.bind(this)
}
componentDidMount () {
window.addEventListener("resize", this.handleResize);
}
componentWillUnmount () {
window.removeEventListener("resize", this.handleResize);
}
// If using Babel, you can avoid the manual binding in the `constructor`
// by using the fat-arrow syntax, like this...
//
// handleResize = () => {...}
//
handleResize () {
this.setState({ width: window.innerWidth });
}
handleClick () {
this.setState({ count: this.state.count + 1 });
}
render () {
var {width, count} = this.state; // ES6 object-expansion shorthand
var {name} = this.props;
return (
<div>
<p>Hi {name}, the window width is currently {width}</p>
<button onClick={this.handleClick}>Clicked {count} Times</button>
</div>
);
}
}
Hooks are still in the proposal phase, but it looks like Facebook is really pushing them hard. It's an attempt to define every component as a function.
Here's the same component rewritten using the useState
and useEffects
hooks.
import React, { useState, useEffect } from "react"; // v16.7-next
function MyResponsiveComponent(props) {
const [count, setCount] = useState(0);
const [width, setWidth] = useState(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
const handleClick = () => setCount(count + 1 );
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
const {name} = props;
return (
<div>
<p>Hi {name}, the window width is currently {width}</p>
<button onClick={handleClick}>Clicked {count} Times</button>
</div>
);
}
Is it shorter? Yep. And that does mean that there is less code to write and review for bugs, but it's not all rainbows and unicorns.
Here are some of the cringe-worthy things I see:
- A lot of (unnecessary) "magic" -- like where
useState
is storing the state - The Hooks rely on the order in which they are called.
- We are re-defining all our methods/functions on every render
- The function passed to
useEffect
returns the teardown function?!?- Why not have the teardown as the second param to
useEffect
? - Or why not pass in an object for clarity?
useEffect({setup, teardown, etc})
- Why not have the teardown as the second param to
- The second param to
useEffect
has really unexpected behavior:- An array of values that should trigger the effect to teardown and rerun
- An empty array makes the effect run once (w/ teardown on component unmount)
- If this param is missing, the effect runs on EVERY render (yikes!)
The end result is that we now have a component defined by a function that is:
- Full of state
- Uses way too much magic
- Has some nasty pitfalls
What kind of pitfalls? If you make a call to a server-side API from inside
useEffect
, and you forget to pass an empty array as the second param, that API
call will fire on every single render. Hopefully after a few thousand calls,
your API will just collapse ;)
In Facebook's Defense, they continue to be very conservative about backward compatibility, and this Hooks approach is opt-in. They say that they plan to continue support for the class-based approach (for now at least).
...but that's missing the point...
Instead of wasting all this effort on Hooks, they could have been solving some real issues. Like, how about adding: routing and high-level state management. There are plenty of (way too many, in fact) unofficial add-on packages, but the integration is clunky and it just creates a bunch of extra setup time and bike-shedding when setting up a new React project.
Maybe it's time to try Vue.js ;)