Skip to content

Instantly share code, notes, and snippets.

@twobitfool
Last active February 23, 2019 17:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save twobitfool/a2d2f962173dd02276c66f3a3c302646 to your computer and use it in GitHub Desktop.
Save twobitfool/a2d2f962173dd02276c66f3a3c302646 to your computer and use it in GitHub Desktop.

React Hooks Dangerous?

Before ranting about Hooks, here's a brief review of React's history...

Functional Components

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>
  )
}

Class-Based Components

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 React Switched to ES6 Classes

Then the React community started to move towards ES6 classes. This was (in general) not too bad, but it came with some costs:

  1. A transpiler became an (almost) "must have" for React work
  2. Methods are no longer automatically bound to this
  3. 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>
    );
  }
}

React "Hooks" the Shark

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})
  • 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:

  1. Full of state
  2. Uses way too much magic
  3. 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 ;)

Aren't Hooks are Optional?

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 ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment