Introduction to react hooks by contrasting with render props

Categories React Hooks
Photo by Cameron Kirby

Hooks… I am so excited about them. It’s not that I can prove they are good things. No. It resembles me more the David Parnas story, the inventor of the information hiding principle.

I repeatedly told him [his supervisor at Philips] that the designs were not “clean” and that they should be “beautiful” in the way that Dijkstra’s designs were beautiful. He did not find this to be a useful comment. He reminded me that we were engineers, not artists. In his mind, I was simply expressing my taste and he wanted to know what criterion I was using in making my judgment. I was unable to tell him.

Trying to explain what “beautiful” and “clean” was Parnas later came up with information hiding principle. I also felt like all these classes, inheritances and render props are not “clean” enough. Hooks at first seemed very strange and not pure but when I tried them in practice, I absolutely loved them. I think there are some principles that hooks help us follow which we are yet to discover.

I’m going to start a series of posts about hooks which will reflect my journey getting them mastered. In this post I’m going to first introduce hooks, then do an overview of core motivations behind them and why to avoid classes.

What are hooks?

Hooks are JavaScript functions which allow a functional React component to declare explicitly its dependencies (hooks) on React environment.

Example 1. Here the Counter component declares that it needs a state provided from the React environment and also Counter subscribes to changes of this state. So if the state changes, React environment must re-render the counter:

function Counter() {
  const [count, setCount] = React.useState(0);
  const increment = () => setCount(c => c + 1);
  return (
    <div>
      <div>{count}</div>
      <div>
        <button onClick={increment}>Increment</button>
      </div>
    </div>
  );
}

Example 2. Now the Counter also declares that it needs the React environment to do a sync with a DOM, setting a document title after each render:

function Counter() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    document.title = count;
  });
  const increment = () => setCount(c => c + 1);
  return (
    <div>
      <div>{count}</div>
      <div>
        <button onClick={increment}>Increment</button>
      </div>
    </div>
  );
}

Why hooks

There are many reasons for introducing hooks. As official documentation states these are 1) simplify components reuse 2) simplify components themselves 3) classes are confusing.

The following small project with contrived but interesting example illustrates how hooks solve reusability and complexity problems when compared to render-props. I have created the following two components which would behave like so (here is code sandbox demo):

rotating square

Render props implementation

The app layout:

function RenderProps() {
  return (
    <section>
      <section
        style={{
          padding: 50
        }}
      >
        <RotorWithRenderProps />
      </section>
      <section>
        <StarsWithRenderProps />
      </section>
    </section>
  );
}

This might not be obvious but rotating square and growing line of starts use the same functionality to power periodic behavior. So I have created a special render props component Period which provides this functionality:

function RotorWithRenderProps() {
  return (
    <Period duration={360} delay={10}>
      {count => <Square angle={count} />}
    </Period>
  );
}

function StarsWithRenderProps() {
  return (
    <Period duration={30} delay={100}>
      {count => <Stars count={count} />}
    </Period>
  );
}

Period needs duration parameter which specifies after which value the period resets and also delay parameter which specifies how often new step in the period should happen.

Here is an iplementationo of Period component:

class PeriodicCounter extends React.Component {
  state = {
    count: 0
  };
  step = () => {
    this.setState(state => ({
      count: (state.count + 1) % this.props.duration
    }));
  };
  render() {
    return this.props.children({
      count: this.state.count,
      step: this.step
    });
  }
}

class Interval extends React.Component {
  interval = null;
  componentDidMount() {
    this.interval = setInterval(() => {
      this.props.step();
    }, this.props.delay);
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  render() {
    return this.props.children();
  }
}

class Period extends React.Component {
  render() {
    return (
      <PeriodicCounter duration={this.props.duration}>
        {({ count, step }) => (
          <Interval delay={this.props.delay} step={step}>
            {() => this.props.children(count)}
          </Interval>
        )}
      </PeriodicCounter>
    );
  }
}

We can see that Period itself uses other render props components. First, Interval its job is to manage interval API and after each tick to call a step function provided to it. Second, PeriodicCounter is a component which is responsible for period managing, counting how many ticks have passed and also resetting the counter when the duration of the period is over.

Hooks implementation

The app layout is the same as for render props:

function Hooks() {
  return (
    <section>
      <section
        style={{
          padding: 50,
          margin: "auto"
        }}
      >
        <RotorWothHooks />
      </section>
      <section>
        <StarsWithHooks />
      </section>
    </section>
  );
}

Implementations of rotor and stars are already much simpler although at first, it might feel intimidating:

function StarsWithHooks() {
  const count = usePeriod({ duration: 30, delay: 100 });
  return <Stars count={count} />;
}

function RotorWothHooks() {
  const count = usePeriod({ duration: 360, delay: 10 });
  return <Square angle={count} />;
}

usePeriod is a user-defined hook. Yes, we can do our own hooks because they are just functions. By usePeriod we sort of inform React to register our requirement to get counts of ticks in the period and to be re-rendered every time when this new tick occurs.

Note that StarsWithHooks and RotorWothHooks both reference the same hook but it does not mean they reference the same state. Each component gets its own state as a result of calling the hook although functionality they get is identical. This is similar to object-oriented programming. In essence, we have class instances, they behave identically but each has different states stored in different locations in memory.

Here is implementation:

function usePeriod({ duration, delay }) {
  const [count, step] = usePeriodicCounter(duration);
  useInterval(() => {
    step();
  }, delay);
  return count;
}

Wow, it seems simple again. First, we make use of another user-defined hook usePeriodicCounter which does the same thing as PeriodicCounter, mainly manages the period, does counting the ticks and resets the counter. It returns step function so that other parts of the code can request the next step of the PeriodicCounter. And this other part of the code is useInterval user-defined hook. It invokes step every delay milliseconds. Since count is the only variable that is returned, usePeriodicCounter changes drive changes in usePeriod and in all its clients. Hence rotors and stars animate.

function useInterval(callback, delay) {
  const ref = React.useRef(null);

  React.useEffect(() => {
    ref.current = callback;
  });

  React.useEffect(() => {
    const interval = setInterval(() => {
      ref.current();
    }, delay);

    const cleanup = () => {
      clearInterval(interval);
    };

    return cleanup;
  }, [delay]);
}

useInterval makes use of useEffectand useRef. Using useRef we can get a variable which references the same memory cell during re-renders. Using useEffect we first always make ref point to the current callback and second setup interval to call referenced callback periodically. The convention is that useEffect‘s first parameter returns the cleanup function which is called when an effect is done: () => clearInterval(interval). Also, usEffect takes a second argument [delay] which specifies the effect’s dependencies. These dependencies when they change cause the effect to re-run. In our case when delay changes, i.e. in current render it differs from the previous render, useEffect is scheduled to re-run.

If you want to learn how useInterval works in greater detail, I highly recommend Dan Abramov’s article on the topic.

function usePeriodicCounter(duration) {
  const [count, setCount] = React.useState(0);
  const step = () => setCount(c => (c + 1) % duration);
  return [count, step];
}

usePeriodicCounter does not have anything new. It uses state and calculates the new state in response to every call of its step function.

Finally here is the whole thing:

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function usePeriodicCounter(duration) {
  const [count, setCount] = React.useState(0);
  const step = () => setCount(c => (c + 1) % duration);
  return [count, step];
}

function useInterval(callback, delay) {
  const ref = React.useRef(null);

  React.useEffect(() => {
    ref.current = callback;
  });

  React.useEffect(() => {
    const interval = setInterval(() => {
      ref.current();
    }, delay);

    const cleanup = () => {
      clearInterval(interval);
    };

    return cleanup;
  }, [delay]);
}

function usePeriod({ duration, delay }) {
  const [count, step] = usePeriodicCounter(duration);
  useInterval(() => {
    step();
  }, delay);
  return count;
}

class PeriodicCounter extends React.Component {
  state = {
    count: 0
  };
  step = () => {
    this.setState(state => ({
      count: (state.count + 1) % this.props.duration
    }));
  };
  render() {
    return this.props.children({
      count: this.state.count,
      step: this.step
    });
  }
}

class Interval extends React.Component {
  interval = null;
  componentDidMount() {
    this.interval = setInterval(() => {
      this.props.step();
    }, this.props.delay);
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  render() {
    return this.props.children();
  }
}

class Period extends React.Component {
  render() {
    return (
      <PeriodicCounter duration={this.props.duration}>
        {({ count, step }) => (
          <Interval delay={this.props.delay} step={step}>
            {() => this.props.children(count)}
          </Interval>
        )}
      </PeriodicCounter>
    );
  }
}

function Hooks() {
  return (
    <section>
      <section
        style={{
          padding: 50,
          margin: "auto"
        }}
      >
        <RotorWothHooks />
      </section>
      <section>
        <StarsWithHooks />
      </section>
    </section>
  );
}

function RenderProps() {
  return (
    <section>
      <section
        style={{
          padding: 50
        }}
      >
        <RotorWithRenderProps />
      </section>
      <section>
        <StarsWithRenderProps />
      </section>
    </section>
  );
}

function StarsWithHooks() {
  const count = usePeriod({ duration: 30, delay: 100 });
  return <Stars count={count} />;
}

function RotorWothHooks() {
  const count = usePeriod({ duration: 360, delay: 10 });
  return <Square angle={count} />;
}

function RotorWithRenderProps() {
  return (
    <Period duration={360} delay={10}>
      {count => <Square angle={count} />}
    </Period>
  );
}

function StarsWithRenderProps() {
  return (
    <Period duration={30} delay={100}>
      {count => <Stars count={count} />}
    </Period>
  );
}

function Square({ angle }) {
  return (
    <div
      style={{
        border: "3px solid green",
        width: 100,
        height: 100,
        transform: `rotate(${angle}deg)`
      }}
    />
  );
}

function Stars({ count }) {
  return (
    <div style={{ height: 100 }}>{[...new Array(count)].map(() => "*  ")}</div>
  );
}

function App() {
  const [impl, setImpl] = React.useState("render-props");
  return (
    <section>
      <div>{impl === "render-props" ? <RenderProps /> : <Hooks />}</div>
      <div>
        {impl === "render-props" ? (
          <button onClick={() => setImpl("hooks")}>Switch to hooks</button>
        ) : (
          <button onClick={() => setImpl("render-props")}>
            Switch to render props
          </button>
        )}
      </div>
    </section>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
FacebooktwitterlinkedinFacebooktwitterlinkedin

Leave a Reply

Your email address will not be published.