Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/pages/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"title": "Hooks"
},
"use-abortable-effect": {},
"use-click-outside": {},
"use-clipboard": {},
"use-component-will-receive-update": {},
"use-composition-input": {},
Expand Down
23 changes: 23 additions & 0 deletions docs/src/pages/use-click-outside.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: useClickOutside
---

# useClickOutside

import ExportMetaInfo from '../components/export-meta-info';

<ExportMetaInfo />

`useClickOutside` calls the callback you provide when a click event occurs outside the element associated with the returned ref

### Usage

```tsx copy
import { useClickOutside } from 'foxact/use-click-outside';

function Component() {
const ref = useClickOutside<HTMLDivHTML>(() => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const ref = useClickOutside<HTMLDivHTML>(() => {
const ref = useClickOutside<HTMLDivElement>(() => {

console.log('clicked outside the div');
});
return <div ref={ref}>...</div>
```
21 changes: 21 additions & 0 deletions src/use-click-outside/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'client-only';
import type { RefObject } from 'react';
import { useRef, useEffect } from 'react';

export function useClickOutside<T extends HTMLElement>(cb: () => void): RefObject<T> {
const ref = useRef<T>(null);

useEffect(() => {
const handleClick = ({ target }: MouseEvent) => {
if (target && ref.current && !ref.current.contains(target as Node)) {
cb();
}
};
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
}, [cb]);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use callback ref instead of useEffect.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you Sukka sensei !
Here is my implementation, please review :)

import 'client-only';
import { useCallback } from 'react';
import type { RefCallback } from 'react';

export function useClickOutside<T extends HTMLElement>(cb: () => void): RefCallback<T> {
  return useCallback((node) => {
    const handleClick = ({ target }: MouseEvent) => {
      if (target && node && !node.contains(target as Node)) {
        cb();
      }
    };
    document.addEventListener('click', handleClick);
    return () => {
      document.removeEventListener('click', handleClick);
    };
  }, [cb]);
}

but as far as I know, ref callback can return a cleanup function only from React 19, before React 19, React would call this callback with null value when unmount. Shouldn't we consider this situation or am I incorrectly implementing this hook? :)


return ref;
}