Refactoring to React Hooks

02/27/20191 Min Read — In React.js

The new version of React, React 16.8 was released this month - and it finally has Hooks.

From the official announcement:

Hooks let you use state and other React features without writing a class. You can also build your own Hooks to share reusable stateful logic between components.

Today I began refactoring my React URL Shortener to using Hooks instead of class components.

The app has a simple form input where you can type in a URL. Submitting the form triggers an API call to the Rebrandly API which shortens the URL.

Originally the Inputform component held its own state. I wanted to avoid triggering a rerendering of the app whenever the user changes the input field.

import React from 'react'
import PropTypes from 'prop-types'

class Inputform extends React.Component {
  static propTypes = {
    onSubmit: PropTypes.func.isRequired
  }
  state = { inputUrl: '' }

  onInputChange = event => {
    event.preventDefault()
    const value = event.target.value
    this.setState({ inputUrl: value })
  }

  onSubmit = event => {
    event.preventDefault()
    this.props.onSubmit({ inputUrl: this.state.inputUrl })
  }

  render() {
    const { inputUrl } = this.state
    return (
      <form className='black-80 tc pv3' onSubmit={this.onSubmit}>
        <label className='f4 dib mb2'>
          URL to be shortened:
          <input
            id='url-shortener-input'
            className='input-reset ma2 mb2 pa2 ba b--black-20'
            placeholder='https://example.com'
            type='text'
            aria-describedby='url-shortener-input-desc'
            value={inputUrl}
            onChange={this.onInputChange}
          />
          <button
            id='url-shortener-input-desc'
            className='fw-30 f6 shadow-5 b--transparent grow br-pill ph3 pv2 mb2 dib white bg-dark-red'
            type='submit'
            disabled={!inputUrl}>
            Shorten
          </button>
        </label>
      </form>
    )
  }
}

export default Inputform

I used this blog post to refactor the app: Using Custom React Hooks to Simplify Forms.

First, I created a new file with a helper function:

import { useState } from 'react'

const useForm = callback => {
  const [values, setValues] = useState({})

  const handleSubmit = event => {
    if (event) event.preventDefault()
    callback(values)
  }

  const handleChange = event => {
    event.persist()
    setValues(values => ({
      ...values,
      [event.target.name]: event.target.value
    }))
  }

  return {
    handleChange,
    handleSubmit,
    values
  }
}

export default useForm

This is almost the same code as in the blog example. The useForm function takes a callback function as an argument and sets up the Hook useState tuple with [values, setValues]. It also defines a handleSubmit function and a handleChange function.
In the handleSubmit function, I made sure to pass the values to the callback function as an argument. This way, the values can be passed to other components.

Now Inputform.js looks like this:

import React from 'react'
import PropTypes from 'prop-types'
import useForm from '../helpers/useform'

const Inputform = ({ onSubmit }) => {
  const { values, handleChange, handleSubmit } = useForm(onSubmit)

  return (
    <form className='black-80 tc pv3' onSubmit={handleSubmit}>
      <label className='f4 dib mb2'>
        URL to be shortened:
        <input
          id='url-shortener-input'
          name='inputUrl'
          className='input-reset ma2 mb2 pa2 ba b--black-20'
          placeholder='https://example.com'
          type='text'
          aria-describedby='url-shortener-input-desc'
          value={values.text}
          onChange={handleChange}
        />
        <button
          id='url-shortener-input-desc'
          className='fw-30 f6 shadow-5 b--transparent grow br-pill ph3 pv2 mb2 dib white bg-dark-red'
          type='submit'
          disabled={!values.inputUrl}>
          Shorten
        </button>
      </label>
    </form>
  )
}

Inputform.propTypes = {
  onSubmit: PropTypes.func.isRequired
}

export default Inputform

It's not a React class component anymore but a stateless functional component. It imports the useForm helper and uses the function to handle state.
The Inputform function receives an onSubmit function from its parent component. This function is ultimately responsible for calling the API. But the Hook handles all the changes to the input form and the baton change to onSubmit. The Inputform is now a "dumb" component and doesn't know anything about state.

Further Reading