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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Gating an effect on open state avoids attaching global listeners you don't need.
- 2Always return a cleanup function from useEffect that mirrors every listener you added.
- 3A generic ref parameter lets one hook serve any HTML element while keeping types accurate.
Related explainers
typescript
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config';
How a JWT strategy authenticates in NestJS
authentication
jwt
passport
Intermediate
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
typescript
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt';
How a GraphQL auth guard works in NestJS
authentication
authorization
jwt
Intermediate
7 steps
typescript
type ResizeCallback = (size: { width: number; height: number }) => void; export function observeResize(callback: ResizeCallback, delay = 150) { let timeoutId: ReturnType<typeof setTimeout> | undefined;
Debouncing window resize in TypeScript
debounce
closures
event-listeners
Intermediate
6 steps
javascript
const MAX_BATCH_SIZE = 20; const FLUSH_INTERVAL = 5000; const ENDPOINT = '/api/analytics';
Batching analytics events in the browser
batching
buffering
sendbeacon
Intermediate
10 steps
typescript
import { Injectable, Logger, OnApplicationShutdown } from '@nestjs/common'; import { InjectRedis } from '@nestjs-modules/ioredis'; import Redis from 'ioredis'; import { DataSource } from 'typeorm';
Graceful shutdown hooks in NestJS
graceful-shutdown
lifecycle-hooks
dependency-injection
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-dismiss-on-outside-click-hook-in-react-explained-typescript-c577/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.