The mysterious useRef hook, how it’s different than createRef…and some bubbles.
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
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:
this.imageRef = React.createRef()
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.
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
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
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!