Intro
Introducing useClickOutside - a handy hook detecting clicks outside a specific HTML element. Ideal for Modals or Sidebars, enhancing app user experience.
Understanding the use cases for the useClickOutside hook
Imagine you've built the Modal or Sidebar component that contains a backdrop behind or you have a Popover component that displays a small menu when clicking the dedicated button.
These components must be closed after clicking the backdrop area (for Modal and Sidebar) or in the case when you have Popover component, you need to detect the moment of click the space out of the Popover trigger element.
To understand it more, please check the gif below with presented situations:
Loading
The use case of hook
So as you saw, we just closed these components after clicking outside their area. Let's create the useClickOutside hook to be able to reuse such logic between different components.
How we'll use useClickOutside hook?
Look at the following code snippet in which we used our hook, and assigned ref to div element, and we passed a callback to the hook configuration that will be triggered when user clicks outside.
Loading
What is really cool? Look how TypeScript will protect us from using invalid elements.
Loading
TypeScript protection
Let's create type definitions for the useClickOutside hook
We need to define a types for the configuration object and for the stuff that will be returned by our hook.
Loading
The most important aspects:
- we imported a type provided by React, that describes the reference object. This ref object may be null. Its value depends on what the parent component does with the ref returned by our hook,
- the configuration object requires a callback that will be called when an outside click will be detected,
- the passed generic type "T" must have a base of HTMLElement, if the parent component will assign the ref value to something else - it's a big mistake and our hook will not work.
Implementing the useClickOutside hook
We'll import previously created types and then, we'll write an implementation that matches created contract.
Loading
Why we've used assertion here? By saying assertion I mean the syntax as Node. It's because the contains method requires type Node but e.target has a type EventTarget | null. This causes TypeScript to yield the following prompt:
Argument of type 'EventTarget | null' is not assignable to parameter of type 'Node | null'.
Is it type-safe? Of course, it's not! We need to improve that with type-guard and when it will determine that the passed element is not a typeof Node, we'll throw an exception - our hook will not work as designed because something with unsupported shape has been passed to the event callback.
Loading
The in operator returns true if a property exists in an object. The !! operator converts the variable to bool, we're doing that because our target parameter may be an object or null. The syntax target is Node is the key here. If the created function will return true it means the type of target will be remembered by TypeScript as a Node type.
We just created our custom type-guard. Now complete implementation looks like this:
Loading
Writting the tests for useClickOutside hook
The most important aspects to test are:
- we need to check if the "mousedown" event is attached and removed,
- we need to verify if the outside click mechanism is working.
We'll use react-testing-library, the fixture pattern, and spies to perform our tests.
If you want to know what is the fixture pattern, please read following article: Creating testing fixtures.
If you want understand the spies role in testing, please read following article: Using spies in React and Typescript.
Let's start from testing the mousedown event mechanism:
Loading
Now we need to use fixture to act like the truthy component is using our hook, we don't have any other option - we want to test a real click outside situation.
Loading
Here you have the complete file with tests:
Loading
The full implementation
Repository to play with. In addition check the full implementation below if you are lazy like me:
Loading
Loading
Loading
What did we learn today?
If you're building your own design system it's really important to write code in the following way. Now, you'll be able to maintain click outside logic with simple, type-safe and tested hook.
- 1. Rendering
16 minutes
Creating portals with custom usePortal hook
2 m
We will change the screens with useStepper hook
3 m
Manage components appearance with useToggle hook
4 m
Removing server warnings for useLayoutEffect with custom hook
3 m
First interaction detection with useOnInteraction hook
4 m
- 2. Forms
4 minutes
- 3. Events
26 minutes
Read the scroll metadata and direction with useScroll hook
5 m
Using clipboard with useClipboard hook
4 m
Detect outside click with the useClickOutside hook
6 m
Deep dive into useIntersectionObserver hook
4 m
Element size measurement with useElementSize hook
7 m
- 4. Guards
5 minutes
- 5. Interactions
5 minutes
Comments
Add your honest opinion about this article and help us improve the content.