The mysterious useRef hook, how it’s different than createRef…and some bubbles.

Photo by Rajesh Ram on Unsplash

The goal of this exercise is to talk about a little known hook called useRef. This hook is similar to the ref system I spoke about in a previous article (link here), but there are some key differences to keep in mind.

To illustrate this hook we’re going to create a dropdown, where the overall goal will be to show a list of options. A user will click on the dropdown and a menu will open up with a list of colors that a user can select…whatever color is selected, some text will show up in that color. Easy enough.

In a nutshell, our App component will have a list of options, in the form of an array of objects, which will be the options a user can select in the dropdown menu. These options will be provided as props down to the Dropdown component and this component is going to use that set of options to decide what it’s going to show.

We’ve added a piece of state to our App component, called selected, which will record what the currently selected option is. That will be provided as a prop to the Dropdown component, so that it knows what the selected value should be.

We’ll add a piece of state to our Dropdown component called open, which we can use to toggle between an open and closed dropdown menu.

We can also use our selected piece of state to make sure that whichever option is selected is displayed only at the top of the menu. Great, that leads us to this:

If you think about the last time you used a dropdown menu…were you able to close it by clicking outside of the dropdown? Chances are very likely that your answer to that question is yes…and if the answer is no, chances are good that you were frustrated by that. In fact, most users expect that they SHOULD be able to close a dropdown by clicking pretty much anywhere. As far as our code goes, though, as of now, that is NOT the case..we can’t click outside of the dropdown to close the menu.

Why is that?

Essentially, it has to do with Event Bubbling…see, even though we are working with React, we always have to remember that that React code exists within an HTML framework. Whenever a user clicks on an element with a React onClick event handler, the event does not stop there. Instead, the event object then travels up to the next parent element…if that element has a click event handler on it, it is automatically invoked. The event object then goes up to the next parent element, and so on and so forth; in every step, the browser checks to see if that element has a click event handler…if it does, it is invoked automatically.

What does this have to do with our component? Well, what we want to happen is for our Dropdown component to detect a click event on any element besides the one it created. Unfortunately, though, that isn’t something that happens naturally with a React component; components find it difficult to set up event handlers on elements that they do not themselves create. In fact, when you are working in React, you usually set up events by assigning some props to a JSX element and passing those props around. However… we can still make use of native browser events and event listeners from our React code!

How? We can actually have our Dropdown component set up a manual event listener on the body element…which would mean that a click on any element will bubble up to the body! Great…but how can we actually get this done?

Hooks to the rescue! We can set up a useEffect hook and inside that hook, whenever our component is first rendered on the screen, we can set up an event listener to listen to the body element.

We want to make sure the arrow function passed into useEffect runs only one time, when we first render our component onto the screen. This is because we only need to set up the event listener once. To do that, our second argument needs to be an empty array.

For the time being, we’ve passed in a console log to test our hook…and it works! No matter where we click on the page, we see:

Something that we must remember is that whichever event listeners we add manually using addEventListener, these event listeners always get called first…after ALL of those are called, then our React event listeners get called, from the most child element up to the most parent.

So in order to complete our component, we need to handle two scenarios. First off, if a user clicks on an element that IS created by our Dropdown component, then we probably do NOT want the body event listener to do anything at all. On the other hand, if a user clicks on any element besides the ones created by our component, we want the body event listener to close the dropdown.

We can always tell what element was clicked on by calling on event.target. But how to we figure out if what we clicked on was created by our component? Yet another hook to the rescue! And we FINALLY get to this “mysterious” hook that I referenced in the title of this post, useRef. As I mentioned at the start, this is somewhat similar to the React.createRef() that I spoke of in another article but not identical. With createRef, you always create a new ref that is generally stored in a class component’s instance property. For example:

You can’t do this with a functional component, so we use useRef to return the same ref on each rendering of that component. This allows the ref to persist between renders, essentially memoizing it, even though you are not technically storing it anywhere.

Just like createRef, though, useRef, allows us to get a reference to a direct DOM element. We’re going to make use of it to get a reference to the most parent element that was created by our Dropdown component, the div with the class of “ui form”. We can then use the contains method on that div…this contains method belongs to all DOM elements and allows us to check if one DOM element is contained within another.

Once we set up that ref object, after our component is rendered for the first time, we can get a reference to that div by making use of ref.current…it is specifically the current property on the ref that is going to give us the reference to that div.

In order to finish this up, we can remove that console log from inside our useEffect and replace it with logic to check whether the element that was clicked on is inside of our component. If it is, then we should do nothing…if it’s not, we need to close the dropdown. Putting all that code together gives us:

And our dropdown is complete!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store