How to create an object and hang onto it for the life of a component using hooks.

In React, the way you do code with class components vs. hooks is completely different. If you are used to classes, things you are comfortable with no longer work like that.

For example: you need to keep hold of an object for the lifetime of your component. How to do that with hooks?

This is typically needed when using third party components or libraries that directly updated the UI (not via React).

What does this look like with a class-based component?

I have come up with a simple example to illustrate what you’d do “before” in the class component world, and what you do “after” using hooks in a functional component world. Let’s see the “before” version.

In this example we have a React component that has it’s own timer. It want’s to set that timer to “tick” every second when the component is displayed.

When the component is removed, we want the timer to stop. Otherwise we could be slowing down the page. It might even cause errors due to the timer action assuming the component still exists.

Here is the example code, with some explanation underneath:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    const id = setInterval(() => {
      console.log('done thing after 1 second')
    }, 1000)
    this.interval = id
  }

  componentWillUnmount() {
    clearInterval(this.interval);
    delete this.interval;
  }

  render() {
    return (
      <div>
        ...
      </div>
    );
  }
}

This is a class based component that sets a timer to do something after 1 second when it mounts. This is in componentDidMount, which is called once the component’s elements, declared in render(), have been inserted into the DOM.

Once the component is about to be removed from the DOM, componentWillUnmount is called. In this method I clear the interval to free up the computer resources and to prevent the timer from running it’s action anymore.

Real world examples will be more complex, but I am keeping it reasonably simple for the puporse of explaining.

How does this now look with hooks?

To do this with hooks, we need to replace componentDidMount, componentWillUnmount and the instance variable of the class. None of this stuff exists in a functional component.

For each of these things this is how you replace them:

ClassesFunctional/Hooks
componentDidMountuseEffect, passing in [] to second argument
componentWillUnmountuseEffect, passing in [] to second argument
Instance variable (i.e. this.interval)useRef

I’m not going to go too much into the useEffect here, there is more information in the React documentation. I might write something about this in future.

What we are focused on is useRef, which is the bit that will replace the member variable in the class.

It is very simple to use. Just call useRef to get a ref hook. Then set it’s current property to the object or value you want to keep a hold of:

const intervalRef = useRef();
intervalRef.current = myObject;

This is different from state because state is more core to the lifecycle of the component. Changing state causes re-renders, and affects other hooks. Also state shouldn’t be mutated but replaced.

Refs on the other hand are for keeping a reference to an object (or a value) that you want to manage outside of React. In this case it is a timer. Refs can be mutated.

The final code with hooks looks like below. Read it carefully. It’s OK if it is confusing, don’t worry too much. There is some guidance below the code.

function MyComponent2 () {
  const intervalRef = useRef();

  useEffect(() => {
    // componentDidMount code goes here:
    const id = setInterval(() => {
      console.log('done thing after 1 second')
    }, 1000)
    intervalRef.current = id;
    
    // componentWillUnmount code goes inside the returned function:
    return () => {
      clearInterval(intervalRef.current);
    }

  // The empty array passed to useEffect means don't run this when dependencies such as props/state change
  // only run it at start up.
  }, []);

  return (
    <div>
      ...
    </div>
  );
}

In the function above, the componentDidMount and componentWillUnmount handler code has been inserted into two call back functions: One that is passed to useEffect and one returned from the function passed to useEffect.

An empy array is also passed to useEffect so that the callbacks are only called during the mount and unmount part of the lifecycle (and not when state changes, etc.).

The useEffect usage might be confusing the first time you see it, so just take it for what it is, you can read more about it in the React documentation, if you want to get a better feel for why this is going on.

Instead of this.interval, we are using intervalRef.current as pretty much as swap-in. To set that ref up there is a call to useRef.

Finally

That’s how you solve it. However there are a couple of loose ends.

First, yes this is complicated, and the hooks solution might seem bizarre. Something so simple is now complicated in hooks. Well I’d argue it’s not complicated – it’s just different. If you are used to classes and learning hooks, you are beginning again, but you will get used to them, and build “muscle memory” for how things are arranged.

Second, with hooks there are often nicer ways to do things. Moving the code over like this solves the problem, but there can be more elegant ways to do things with hooks.

For example you can create a hook that does all of the timer stuff for you, and then your component is freed from having lots of ugly timer code inside of it. If you are interested in learning more, see this article from Dan Abramov: https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Posted by Martin Capodici

.

Leave a Comment

Your email address will not be published. Required fields are marked *