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:
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.
What is really cool? Look how TypeScript will protect us from using invalid elements.
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.
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.
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.
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:
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:
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.
Here you have the complete file with tests:
The full implementation
Repository to play with. In addition check the full implementation below if you are lazy like me:
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
Creating portals with custom usePortal hook
We will change the screens with useStepper hook
Manage components appearance with useToggle hook
Removing server warnings for useLayoutEffect with custom hook
First interaction detection with useOnInteraction hook
- 2. Forms
- 3. Events
Read the scroll metadata and direction with useScroll hook
Using clipboard with useClipboard hook
Detect outside click with the useClickOutside hook
Deep dive into useIntersectionObserver hook
Element size measurement with useElementSize hook
- 4. Guards
- 5. Interactions