Why React Hooks?

Tyler McGinnis - Jul 30 '19 - - Dev Community

This was originally published at ui.dev and is part of our React Hooks course. If you enjoy this post, check it out.


The first thing you should do whenever you're about to learn something new is ask yourself two questions -

1) Why does this thing exist?
2) What problems does this thing solve?

If you never develop a convincing answer for both of those questions, you won't have a solid enough foundation to build upon when you dive into the specifics. These questions are specifically interesting in regards to React Hooks. React was the most popular and most loved front-end framework in the JavaScript ecosystem when Hooks were released. Despite the existing praise, the React team still saw it necessary to build and release Hooks. Lost in the various Medium posts and blog think pieces on Hooks are the reasons (1) why and for what (2) benefit, despite high praise and popularity, the React team decided to spend valuable resources building and releasing Hooks. To better understand the answers to both of these questions, we first need to take a deeper look into how we've historically written React apps.

createClass

If you've been around the React game long enough, you'll remember the React.createClass API. It was the original way in which we'd create React components. All of the information you'd use to describe the component would be passed as an object to createClass.

const ReposGrid = React.createClass({
  getInitialState () {
    return {
      repos: [],
      loading: true
    }
  },
  componentDidMount () {
    this.updateRepos(this.props.id)
  },
  componentDidUpdate (prevProps) {
    if (prevProps.id !== this.props.id) {
      this.updateRepos(this.props.id)
    }
  },
  updateRepos (id) {
    this.setState({ loading: true })

    fetchRepos(id)
      .then((repos) => this.setState({
        repos,
        loading: false
      }))
  },
  render() {
    const { loading, repos } = this.state

    if (loading === true) {
      return <Loading />
    }

    return (
      <ul>
        {repos.map(({ name, handle, stars, url }) => (
          <li key={name}>
            <ul>
              <li><a href={url}>{name}</a></li>
              <li>@{handle}</li>
              <li>{stars} stars</li>
            </ul>
          </li>
        ))}
      </ul>
    )
  }
})
Enter fullscreen mode Exit fullscreen mode

💻 Play with the code.

createClass was a simple and effective way to create React components. The reason React initially used the createClass API was because, at the time, JavaScript didn't have a built-in class system. Of course, this eventually changed. With ES6, JavaScript introduced the class keyword and with it a native way to create classes in JavaScript. This put React in a tough position. Either continue using createClass and fight against the progression of JavaScript or submit to the will of the EcmaScript standard and embrace classes. As history has shown, they chose the later.

React.Component

We figured that we’re not in the business of designing a class system. We just want to use whatever is the idiomatic JavaScript way of creating classes. - React v0.13.0 Release

React v0.13.0 introduced the React.Component API which allowed you to create React components from (now) native JavaScript classes. This was a big win as it better aligned React with the EcmaScript standard.

class ReposGrid extends React.Component {
  constructor (props) {
    super(props)

    this.state = {
      repos: [],
      loading: true
    }

    this.updateRepos = this.updateRepos.bind(this)
  }
  componentDidMount () {
    this.updateRepos(this.props.id)
  }
  componentDidUpdate (prevProps) {
    if (prevProps.id !== this.props.id) {
      this.updateRepos(this.props.id)
    }
  }
  updateRepos (id) {
    this.setState({ loading: true })

    fetchRepos(id)
      .then((repos) => this.setState({
        repos,
        loading: false
      }))
  }
  render() {
    if (this.state.loading === true) {
      return <Loading />
    }

    return (
      <ul>
        {this.state.repos.map(({ name, handle, stars, url }) => (
          <li key={name}>
            <ul>
              <li><a href={url}>{name}</a></li>
              <li>@{handle}</li>
              <li>{stars} stars</li>
            </ul>
          </li>
        ))}
      </ul>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

💻 Play with the code.

Though a clear step in the right direction, React.Component wasn't without its trade-offs.

constructor

With Class components, you initialize the state of the component inside of the constructor method as a state property on the instance (this). However, according to the ECMAScript spec, if you're extending a subclass (in this case, React.Component), you must first invoke super before you can use this. Specifically, when using React, you also have to remember to pass props to super.

  constructor (props) {
    super(props) // 🤮

    ...
  }
Enter fullscreen mode Exit fullscreen mode
Autobinding

When using createClass, React would auto-magically bind all the methods to the component's instance, this. With React.Component, that wasn't the case. Very quickly, React developers everywhere realized they didn't know how the this keyword worked. Instead of having method invocations that "just worked", you had to remember to .bind methods in the class's constructor. If you didn't, you'd get the popular "Cannot read property setState of undefined" error.

  constructor (props) {
    ...

    this.updateRepos = this.updateRepos.bind(this) // 😭
  }
Enter fullscreen mode Exit fullscreen mode

Now I know what you might be thinking. First, these issues are pretty superficial. Sure calling super(props) and remembering to bind your methods is annoying, but there's nothing fundamentally wrong here. Second, these aren't necessarily even issues with React as much as they are with the way JavaScript classes were designed. Both points are valid. However, we're developers. Even the most superficial issues become a nuisance when you're dealing with them 20+ times a day. Luckily for us, shortly after the switch from createClass to React.Component, the Class Fields proposal was created.

Class Fields

Class fields allow you to add instance properties directly as a property on a class without having to use constructor. What that means for us is that with Class Fields, both of our "superficial" issues we previously talked about would be solved. We no longer need to use constructor to set the initial state of the component and we no longer need to .bind in the constructor since we could use arrow functions for our methods.

class ReposGrid extends React.Component {
  state = {
    repos: [],
    loading: true
  }
  componentDidMount () {
    this.updateRepos(this.props.id)
  }
  componentDidUpdate (prevProps) {
    if (prevProps.id !== this.props.id) {
      this.updateRepos(this.props.id)
    }
  }
  updateRepos = (id) => {
    this.setState({ loading: true })

    fetchRepos(id)
      .then((repos) => this.setState({
        repos,
        loading: false
      }))
  }
  render() {
    const { loading, repos } = this.state

    if (loading === true) {
      return <Loading />
    }

    return (
      <ul>
        {repos.map(({ name, handle, stars, url }) => (
          <li key={name}>
            <ul>
              <li><a href={url}>{name}</a></li>
              <li>@{handle}</li>
              <li>{stars} stars</li>
            </ul>
          </li>
        ))}
      </ul>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

💻 Play with the code.

So now we're good, right? Unfortunately, no. The move from createClass to React.Component came with some tradeoffs, but as we saw, Class Fields took care of those. Unfortunately, there are still some more profound (but less talked about) issues that exist with all the previous versions we've seen.

The whole idea of React is that you're better able to manage the complexity of your application by breaking it down into separate components that you then can compose together. This component model is what makes React so elegant. It's what makes React, React. The problem, however, doesn't lie in the component model, but in how the component model is implemented.

Duplicate Logic

Historically, how we've structured our React components has been coupled to the component's lifecycle. This divide naturally forces us to sprinkle related logic throughout the component. We can clearly see this in the ReposGrid example we've been using. We need three separate methods (componentDidMount, componentDidUpdate, and updateRepos) to accomplish the same thing - keep repos in sync with whatever props.id is.

  componentDidMount () {
    this.updateRepos(this.props.id)
  }
  componentDidUpdate (prevProps) {
    if (prevProps.id !== this.props.id) {
      this.updateRepos(this.props.id)
    }
  }
  updateRepos = (id) => {
    this.setState({ loading: true })

    fetchRepos(id)
      .then((repos) => this.setState({
        repos,
        loading: false
      }))
  }
Enter fullscreen mode Exit fullscreen mode

To fix this, we'd need a whole new paradigm for the way in which we'd handle side effects in React components.

Sharing Non-visual Logic

When you think about composition in React, odds are you think in terms of UI composition. This is natural since it's what React is so good at.

view = fn(state)
Enter fullscreen mode Exit fullscreen mode

Realistically, there's more to building an app than just the UI layer. It's not uncommon to need to compose and reuse non-visual logic. However, because React couples UI to a component, this can be difficult. Historically, React hasn't had a great answer for this.

Sticking with our example, say we needed to create another component that also needed the repos state. Right now, that state and the logic for handling it lives inside of the ReposGrid component. How would we approach this? Well, the simplest approach would be to copy all of the logic for fetching and handling our repos and paste it into the new component. Tempting, but nah. A smarter approach would be to create a Higher-Order Component that encapsulated all of the shared logic and passed loading and repos as props to whatever component needed it.

function withRepos (Component) {
  return class WithRepos extends React.Component {
    state = {
      repos: [],
      loading: true
    }
    componentDidMount () {
      this.updateRepos(this.props.id)
    }
    componentDidUpdate (prevProps) {
      if (prevProps.id !== this.props.id) {
        this.updateRepos(this.props.id)
      }
    }
    updateRepos = (id) => {
      this.setState({ loading: true })

      fetchRepos(id)
        .then((repos) => this.setState({
          repos,
          loading: false
        }))
    }
    render () {
      return (
        <Component
          {...this.props}
          {...this.state}
        />
      )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now whenever any component in our app needed repos (or loading), we could wrap it in our withRepos HOC.

// ReposGrid.js
function ReposGrid ({ loading, repos }) {
  ...
}

export default withRepos(ReposGrid)
Enter fullscreen mode Exit fullscreen mode
// Profile.js
function Profile ({ loading, repos }) {
  ...
}

export default withRepos(Profile)
Enter fullscreen mode Exit fullscreen mode

💻 Play with the code.

This works and historically (along with Render Props) has been the recommended solution for sharing non-visual logic. However, both these patterns have some downsides.

First, if you're not familiar with them (and even when you are), your brain can get a little wonky following the logic. With our withRepos HOC, we have a function that takes the eventually rendered component as the first argument but returns a new class component which is where our logic lives. What a convoluted process.

Next, what if we had more than one HOC we were consuming. As you can imagine, it gets out of hand pretty quickly.

export default withHover(
  withTheme(
    withAuth(
      withRepos(Profile)
    )
  )
)
Enter fullscreen mode Exit fullscreen mode

Worse than ^ is what eventually gets rendered. HOCs (and similar patterns) force you to restructure and wrap your components. This can eventually lead to "wrapper hell" which again, makes it harder to follow.

<WithHover>
  <WithTheme hovering={false}>
    <WithAuth hovering={false} theme='dark'>
      <WithRepos hovering={false} theme='dark' authed={true}>
        <Profile 
          id='JavaScript'
          loading={true} 
          repos={[]}
          authed={true}
          theme='dark'
          hovering={false}
        />
      </WithRepos>
    </WithAuth>
  <WithTheme>
</WithHover>
Enter fullscreen mode Exit fullscreen mode

Current State

So here's where we're at.

  • React is hella popular.
  • We use Classes for React components cause that's what made the most sense at the time.
  • Calling super(props) is annoying.
  • No one knows how "this" works.
  • OK, calm down. I know YOU know how "this" works, but it's an unnecessary hurdle for some.
  • Organizing our components by lifecycle methods forces us to sprinkle related logic throughout our components.
  • React has no good primitive for sharing non-visual logic.

Now we need a new component API that solves all of those problems while remaining simple, composable, flexible, and extendable. Quite the task, but somehow the React team pulled it off.

React Hooks

Since React v0.14.0, we've had two ways to create components - classes or functions. The difference was that if our component had state or needed to utilize a lifecycle method, we had to use a class. Otherwise, if it just accepted props and rendered some UI, we could use a function.

Now, what if this wasn't the case. What if instead of ever having to use a class, we could just always use a function.

Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.

- John Carmack. Oculus VR CTO.

Sure we'd need to figure out a way to add the ability for functional components to have state and lifecycle methods, but assuming we did that, what benefits would we see?

Well, we would no longer have to call super(props), we'd no longer need to worry about binding our methods or the this keyword, and we'd no longer have a use for Class Fields. Essentially, all of the "superficial" issues we talked about earlier would go away.

(ノಥ,_)ノ彡 React.Component 🗑

function (Ő‿Ő)
Enter fullscreen mode Exit fullscreen mode

Now, the harder issues.

  • State
  • Lifecycle methods
  • Sharing non-visual logic
State

Since we're no longer using classes or this, we need a new way to add and manage state inside of our components. As of React v16.8.0, React gives us this new way via the useState method.

useState is the first of many "Hooks" you'll be seeing in this course. Let the rest of this post serve as a soft introduction. We'll be diving much deeper into useState as well as other Hooks in future sections.

useState takes in a single argument, the initial value for the state. What it returns is an array with the first item being the piece of state and the second item being a function to update that state.

const loadingTuple = React.useState(true)
const loading = loadingTuple[0]
const setLoading = loadingTuple[1]

...

loading // true
setLoading(false)
loading // false
Enter fullscreen mode Exit fullscreen mode

As you can see, grabbing each item in the array individually isn't the best developer experience. This is just to demonstrate how useState returns an array. Typically, you'd use Array Destructuring to grab the values in one line.

// const loadingTuple = React.useState(true)
// const loading = loadingTuple[0]
// const setLoading = loadingTuple[1]

const [ loading, setLoading ] = React.useState(true) // 👌
Enter fullscreen mode Exit fullscreen mode

Now let's update our ReposGrid component with our new found knowledge of the useState Hook.

function ReposGrid ({ id }) {
  const [ repos, setRepos ] = React.useState([])
  const [ loading, setLoading ] = React.useState(true)

  if (loading === true) {
    return <Loading />
  }

  return (
    <ul>
      {repos.map(({ name, handle, stars, url }) => (
        <li key={name}>
          <ul>
            <li><a href={url}>{name}</a></li>
            <li>@{handle}</li>
            <li>{stars} stars</li>
          </ul>
        </li>
      ))}
    </ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

💻 Play with the code.

  • State ✅
  • Lifecycle methods
  • Sharing non-visual logic
Lifecycle methods

Here's something that may make you sad (or happy?). When using React Hooks, I want you to take everything you know about the traditional React lifecycle methods as well as that way of thinking, and forget it. We've already seen the problem of thinking in terms of the lifecycle of a component - "This [lifecycle] divide naturally forces us to sprinkle related logic throughout the component." Instead, think in terms of synchronization.

Think of any time you've ever used a lifecycle event. Whether it was to set the initial state of the component, fetch data, update the DOM, anything - the end goal was always synchronization. Typically, synchronizing something outside of React land (an API request, the DOM, etc.) with something inside of React land (component state) or vice versa.

When we think in terms of synchronization instead of lifecycle events, it allows us to group together related pieces of logic. To do this, React gives us another Hook called useEffect.

Defined, useEffect lets you perform side effects in function components. It takes two arguments, a function, and an optional array. The function defines which side effects to run and the (optional) array defines when to "re-sync" (or re-run) the effect.

React.useEffect(() => {
  document.title = `Hello, ${username}`
}, [username])
Enter fullscreen mode Exit fullscreen mode

In the code above, the function passed to useEffect will run whenever username changes. Therefore, syncing the document's title with whatever Hello, ${username} resolves to.

Now, how can we use the useEffect Hook inside of our code to sync repos with our fetchRepos API request?

function ReposGrid ({ id }) {
  const [ repos, setRepos ] = React.useState([])
  const [ loading, setLoading ] = React.useState(true)

  React.useEffect(() => {
    setLoading(true)

    fetchRepos(id)
      .then((repos) => {
        setRepos(repos)
        setLoading(false)
      })
  }, [id])

  if (loading === true) {
    return <Loading />
  }

  return (
    <ul>
      {repos.map(({ name, handle, stars, url }) => (
        <li key={name}>
          <ul>
            <li><a href={url}>{name}</a></li>
            <li>@{handle}</li>
            <li>{stars} stars</li>
          </ul>
        </li>
      ))}
    </ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

💻 Play with the code.

Pretty slick, right? We've successfully gotten rid of React.Component, constructor, super, this and more importantly, we no longer have our effect logic sprinkled (and duplicated) throughout the component.

  • State ✅
  • Lifecycle methods ✅
  • Sharing non-visual logic
Sharing non-visual logic

Earlier we mentioned that the reason React didn't have a great answer to sharing non-visual logic was because "React couples UI to a component". This lead to overcomplicated patterns like Higher-order components or Render props. As you can probably guess by now, Hooks have an answer for this too. However, it's probably not what you think. There's no built-in Hook for sharing non-visual logic, instead, you can create your own custom Hooks that are decoupled from any UI.

We can see this in action by creating our own custom useRepos Hook. This Hook will take in an id of the Repos we want to fetch and (to stick to a similar API) will return an array with the first item being the loading state and the second item being the repos state.

function useRepos (id) {
  const [ repos, setRepos ] = React.useState([])
  const [ loading, setLoading ] = React.useState(true)

  React.useEffect(() => {
    setLoading(true)

    fetchRepos(id)
      .then((repos) => {
        setRepos(repos)
        setLoading(false)
      })
  }, [id])

  return [ loading, repos ]
}
Enter fullscreen mode Exit fullscreen mode

What's nice is any logic that's related to fetching our repos can be abstracted inside of this custom Hook. Now, regardless of which component we're in and even though it's non-visual logic, whenever we need data regarding repos, we can consume our useRepos custom Hook.

function ReposGrid ({ id }) {
  const [ loading, repos ] = useRepos(id)

  ...
}
Enter fullscreen mode Exit fullscreen mode
function Profile ({ user }) {
  const [ loading, repos ] = useRepos(user.id)

  ...
}
Enter fullscreen mode Exit fullscreen mode

💻 Play with the code.

  • State ✅
  • Lifecycle methods ✅
  • Sharing non-visual logic ✅

The marketing pitch for Hooks is that you're able to use state inside function components. In reality, Hooks are much more than that. They're about improved code reuse, composition, and better defaults. There's a lot more to Hooks we still need to cover, but now that you know WHY they exist, we have a solid foundation to build on.


This was originally published at TylerMcGinnis.com and is part of our React Hooks course. If you enjoyed this post, check it out.

. . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player