React: Detecting Clicks Outside of a Component
Partially stolen, this, but I thought I’d write about it as I can come back to it and put my own spin on it. I don’t even know if this is going to work.
I have a date picker which I’ve created. It might not be perfect but it looks like this, just to give you something to see in your brains:
The goal here for me is to get the thing to close when you click outside of the calendar. One easy way to achieve this would be to have the visibility passed in as a prop. Then when a user selects a date by hitting the OK button, a function, passed in as a prop, could be called, and since that function wouldn’t live inside the calendar but inside the parent, the value of the prop passed to the date picker could be changed.
I didn’t like this idea though. I mean, sure I’ve done it before, but wouldn’t it be nice if the calendar could just take care of itself in this respect? I mean there aren’t any dependencies here, really, and that’s what props are for. Or rather there are dependencies, but it’s a silly one; all we need to know is when the user’s clicked on something that’s not the calendar.
The other option, and the option I shall be stealing for you tonight, is to have a generic solution. In the form of a custom hook. A custom hook is just a function but it’s allowed to use React hooks which you can’t do in a normal function. Sadly, despite spending fifty hours going through a course in React, the knowledge has begun to evaporate, but that much I do remember. The theory of this whole thing will be explained at the end of this tutorial because I don’t quite know where we’re going with this either. Here’s my vague understanding though: nope…you’ll have to wait.
So the first step is to write something that detects the click. The tutorial says that a naïve approach might be to add a handler to the outermost parent to the thing we’re detecting our outside click on. This sounds like a poor idea to me because the outermost parent might not take up the whole screen and as the tutorial suggests, it might change anyway. So we won’t do that.
Instead the suggestion is to add it to the document element. Let’s implement the suggested hook first. This hook is just a function with the ability to call React’s internal hooks and also with the ability to take arguments. So I’ll create a new folder in my src directory, create a file named useOutsideClick.js and add the following implementation:
This isn’t a full implementation since we’re not yet using the ref. However, you can see that wherever this hook is used we will instantiate one, ready to be used, and when the component that uses this hook is rendered, we’ll set up a handleClick method which will call our passed in function, and we’re also setting up the click to be called whenever the document is clicked. Doesn’t feel very Reacty, but I’ll use it. I mean it’s not tonnes of state, anyway, and it’s a solution which will probably work.
So how does this hook fit in with my date picker? Well, I have a DatePicker component. This includes a Calendar component which is the Calendar which gets displayed when the date picker tells it to. The DatePicker has some state called showCalendar which is passed to the calendar, but the calendar can now tell the DatePicker to update this state, it then gets passed back down to the calendar and hides or shows it. I’ve added a new method to the DatePicker specifically for hiding the calender.
If you take my word for it that I’ve added ref={ref}, as in that reference above which was returned from our hook onto the button, then that’s where we are. I am quite confused at this point but hopefully it’ll all start to make sense.
Okay, so…it looks to me that the ref is added to get hold of the calendar. That’s fine but it’s the document that we want. We can update the hook to ensure that the click didn’t come from the calendar, however. That’s interesting.
Okay! Something’s clicked. Since the calendar is what ref points to, here we are checking that if the document received a click, which is plausible due to event bubbling, then check to see if it came from the calendar and if it didn’t, then hide the calendar. Since that’s what we’ve passed in as the argument. This makes some sense.
Well I’ve not tested this and I expect I’ve made an accident somewhere but let’s test this out. It bloody works you know. Time will tell if it breaks anything else, but for now I am happy. Plus I’ve achieved something in between 101 Dalmatians.
So the whole thing explained here:
Inside of the component, the Calendar, that we want to detect the change in we create a ref. The ref is actually initialised within our custom hook, but it’s just a ref, the same as any other, and initialised inside the hook with useRef.
That ref is placed on the element that we want to be able to detect the click away from.
Now inside the custom hook we now have access to that ref. This allows us to add an event handler to the document and only call a passed in function if the event did not come from that ref.
Pretty simple in the end! I do need to learn more about React, but I am not doing fifty hours of course again, at least not until it’s updated. It does have its uses, though, I mean I’d never have got this far with my project if I hadn’t done it without really looking anything up.
Anyway, thank you for reading, God Speed and good night. Back to the Dalmatians…