How to Use useEffect (and other hooks) in Class Components

Hooks are a great new feature in React. The first initial case I found them useful was where we had to create class components just because we wanted to use a single ref or store one variable in state. Now in those cases, we can use hooks to write more succinct code. Here’s a quick example using useState :

Using useState

Old and Busted

class SearchBar extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      query: ''
    }
  }

  render() {
    return (
      <form action="https://duckduckgo.com">
        <input
          type="text"
          name="q"
          onChange={(e) => { this.setState({ query: e.target.value }) }}
        />
        <input type="submit" value="search" />
      </form>
    )
  }
}

New Hotness

const SearchBar = () => {
  const [query, setQuery] = useState('')
  return (
    <form action="https://duckduckgo.com" >
      <input
        type="text"
        name="q"
        onChange={(e) => { setQuery(e.target.value) }}
      />
      <input type="submit" value="search" />
    </form>
  )
}

Thinking in Effects

useRef is similar. However, working with useEffectis a bit different. It’s designed to replace several lifecycle methods, such as componentWillReceiveProps, componentWillMount, and componentWillUnmount. It doesn’t map one-to-one to old functionality like state and ref, which makes it more difficult to “think in effects.”

Here’s what finally made it click for me: the first two arguments of useEffect are a function (didUpdate), and an array of dependencies. the didUpdate function can return a function of its own, a cleanUp function. When a component mounts or the dependencies are updated, didUpdate is called. When the component unmounts, cleanUp is called if present.

Once you grok how effects work (“when this variable changes, do something”) over the lifecycle methods, you can save yourself a lot of headaches in code. You no longer have to write code like this:

Old and Busted: componentWillReceiveProps

  componentWillReceiveProps = (nextProps) => {
    if (nextProps.position !== this.props.position) {
      this.moveMap(nextProps.position)
    }
  }

You can now write more succicnt statements like this:

New Hotness: useEffect

useEffect(() => { moveMap(position) }, [position])

Here’s another example is using useEffect to replace componentDidMount and componentWillUnmount for setting and clearing event listeners. By declaring the dependencies array as empty, you only call the didUpdate and cleanUp functions once each. No dependencies mean no updates.

Old and Busted: componentWill(un)mount

  componentWillMount = () => {
    /* attach listeners to google StreetView */
    const streetView = this.getStreetView()
    window.google.maps.event.addListener(streetView, 'zoomChanged', this.handlePovChanged())
  }

  componentWillUnmount = () => {
    window.google.maps.event.clearInstanceListeners()
  }

  getStreetView = () => { /* ... */ }
  handlePovChanged = () => { /* ... */ }

New Hotness: useEffect

  const getStreetView = () => { /* ... */ }
  const handlePovChanged = () => { /* ... */ }
  const { addListener, clearInstanceListeners } = window.google.maps.event
  
  useEffect(() => {
    const streetView = getStreetView()
    addListener(streetView, 'on', handlePovChanged())
    return clearInstanceListeners
  }, []) // empty dependency array = only called on mount and unmount

Instead of grouping functionality by when they are in the lifecycle, you can group them by concern. It makes your code easier to reason about.

There’s one problem. You can’t use useEffect (or any other hook) in a class component. Hooks are only available in functional components. If you want to refactor your lifecycle methods to use useEffect, you have to refactor entire class components writ large. This is both time-consuming and prone to error. What if you could refactor just this one part of the code?

Use Hooks With Adapter Components

Considering that:

  • we cannot use hooks within class components…
  • we can use hooks within functional components…
  • And we can use functional components within class components…

We can see a solution: create a functional component that encapsulates the useEffect behavior, and use that in your class components! It’s a take on the adapter pattern from object-oriented programming: We create a wrapper that encapsulates the functionality of a piece of code (useEffect), while changing its interface. A hook in component’s clothing. 🐺

Here’s a simple one I use to replace componentWillReceiveProps checks like the one above.

Observer: useEffect Adapter Component

const observer = ({ value, didUpdate }) => {
  useEffect(() => {
    didUpdate(value)
  }, [value])
  return null // component does not render anything
}

Example Usage

<Observer value={this.props.position} didUpdate={this.moveMap} />

You don’t have to pass the argument to didUpdate, but in this case I found that the easiest way to have access to that variable. You could also create a similar component to handle mounting and unmounting behavior.

MountHandler: another useEffect Adapter

const mountHandler = ({ onMount, onUnMount }) => {
  useEffect(() => {
    onMount()
    return onUnMount
  },[])
  return null
}

You could combine these into one component, or create more specific adapters that encase more business logic themselves. Do what works best for your codebase. Ideally, these a stop-gap solution that help you refactor class-based components incrementally. Here’s the strategy I used:

  1. Identify a class-based component you want to refactor.
  2. Isolate part of the class-specific code.
  3. Create an adapter component that allows you to remove that class specific code (for example, lifecycle methods)
  4. Run tests, ensure that all functionality still works.
  5. Repeat until you have removed all class-specific code.
  6. Convert the component from a class to a function.

In the end, You end up with code thats easier to reason about and more performant. Hope this helps!