typescript 39 lines · 7 steps

A dismiss-on-outside-click hook in React

A reusable React hook that closes a menu or popover when the user clicks elsewhere or presses Escape.

Explained by highlit
1import { useEffect, useRef, useState } from "react";
2 
3export function useClickOutside<T extends HTMLElement>() {
4 const ref = useRef<T>(null);
5 const [isOpen, setIsOpen] = useState(false);
6 
7 useEffect(() => {
8 if (!isOpen) return;
9 
10 function handlePointerDown(event: PointerEvent) {
11 const target = event.target as Node;
12 if (ref.current && !ref.current.contains(target)) {
13 setIsOpen(false);
14 }
15 }
16 
17 function handleKeyDown(event: KeyboardEvent) {
18 if (event.key === "Escape") {
19 setIsOpen(false);
20 }
21 }
22 
23 document.addEventListener("pointerdown", handlePointerDown);
24 document.addEventListener("keydown", handleKeyDown);
25 
26 return () => {
27 document.removeEventListener("pointerdown", handlePointerDown);
28 document.removeEventListener("keydown", handleKeyDown);
29 };
30 }, [isOpen]);
31 
32 return {
33 ref,
34 isOpen,
35 open: () => setIsOpen(true),
36 close: () => setIsOpen(false),
37 toggle: () => setIsOpen((prev) => !prev),
38 };
39}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Gating an effect on open state avoids attaching global listeners you don't need.
  2. 2Always return a cleanup function from useEffect that mirrors every listener you added.
  3. 3A generic ref parameter lets one hook serve any HTML element while keeping types accurate.

Related explainers

typescript
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react';
 
const cache = new Map();
const inflight = new Map();

Building a stale-while-revalidate hook in React

caching request-deduplication custom-hooks
Advanced 10 steps
typescript
import { useEffect, useState } from "react";
 
interface Section {
  id: string;

Building a scroll-spy hook in React

custom-hooks intersectionobserver dom-observation
Intermediate 8 steps

Share this explainer

Here's the card — post it anywhere.

A dismiss-on-outside-click hook in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code