How to use React refs with useEffect hooks

Fernando Beck
Tech @ Eatwith
Published in
4 min readOct 9, 2019
Photo by Efe Kurnaz on Unsplash

After using React for a few years and getting used to writing components thinking in classes and lifecycle methods, the new React hooks are completely changing the game. It’s not easy having to ‘unlearn’ something and question the way you build your components. Despite the React team not planning to remove support for class components in the near future, it’s clear that functional components and hooks is the new way of building components.

At Eatwith, we have been using React for more than 3 years now, which means that most of our codebase is written using lifecycles and classes. We are continuously refactoring our codebase to keep our tech debt under control and part of this work includes increasingly using hooks instead of lifecycle methods.

This relearning process however is not always easy as the team gets used to the new paradigm and we share knowledge on how to make the most of React. In this article I’ll share the learnings I made following an interesting case we faced recently.

To prevent being distracted by unnecessary details I have made a few simplifications and won’t detail some helpers, design and wrappers. If you’re interested however in taking a look in the code used here, you can find it all in the companion repository.

The case study

The component we are going to study is a simple ‘Read more’ block.

Interface

  • children: content that should be wrapped (must be a React node so we can attach a ref to it).
  • height: maximum height before wrapping the content.

Behaviour

  • If the content’s height is larger than the height wrap the content and show a ‘Read more’ button.
  • Once the user has clicked the ‘Read more’ button, it should stay unwrapped.

Notes

The content rendered inside the ‘Read more’ wrapper could change over time for example after being loaded asynchronously.

Class and lifecycle methods component

The component should observe the size of its children prop by attaching a ref to it and then use React’s lifecycle methods to update the state.

As explained in more details in this answer from Dan Abramov on StackOverflow, refs are guaranteed to be set before componentDidMount and componentDidUpdate.

There are 2 state variables, one to keep track if the user has already clicked the ‘Read more’ button and another that is set in the shouldWrapIfTooTall, which in its turn is called in both componentDidMount and componentDidUpdate lifecycle methods.

The hooks refactor

In order to refactor our originally class component into a functional component, we will need

  • useState to replace our component’s state variables isWrapped and hasUserUnwrapped
  • useEffect to replace the component’s lifecycle methods componentDidMount and componentDidUpdate
  • useRef to create the ref to attach to the children props

After refactoring the component to use the new hooks the result is:

If we check the result of the code we just made below, we can see that it works as it should when the content loaded synchronously, but the content does not get wrapped when content is load asynchronously.

The reason why this happens is that the useEffect is not being called when the children prop change (and hence the contentRef). In spite of passing the contentRef as a dependency to the useEffect hook, the effect is not tracking correctly the change in the current property.

So intuition would say that in order to solve the problem, all that has to be done is adding replacing contentRef by contentRef.current in the useEffect dependency. You can see the commit’s diff here

The result of this small change is not all bad, because we can see that the component now work as it should when data is loaded both synchronously and asynchronously. However, by looking at our terminal we see now that there’s a new warning:

React Hook useEffect has an unnecessary dependency: ‘contentRef.current’. Either exclude it or remove the dependency array. Mutable values like ‘contentRef.current’ aren’t valid dependencies because mutating them doesn’t re-render the component

If we look closely at what the warning says, it says that contentRef.current is not a valid dependency, because mutating it will not rerender the component. In our case, however, we’re not changing the value of contentRef.current, simply using its value to decide if we should update the state, which in turn would trigger a rerender.

Being still early in our journey into hooks and learning how to get the handle of it, we try and pay attention to warnings given by React, because we assume they have been added to prevent unexpected behaviour to happen, so how could we refactor our component to remove contentRef.current from the dependencies array and still have our component to function as it should?

The answer to this question requires 2 small changes in the implementation of our component:

  1. Replace contentRef.current in the useEffect by an auxiliary variable
  2. Replace the useRef by a callback ref, which is responsible for setting the value the new variable contentHeight in the state

You can see the commit’s diff here.By adding this new state variable, we managed to achieve what we wanted and our component now works as expected in both synchronous and asynchronous cases.

Do you have an idea of how we could do this differently? Any question? Please drop a comment and I’ll try to help the best I can!

Check out the complete code for implementing useEffect with refs on the GitHub repo. Then clap and leave a comment below!

Eatwith brings people together through food at tables all around the world. For more articles and insights from the tech & product team, please subscribe to the Tech @ Eatwith blog.

--

--