Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/giant-boats-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codiume/hooks": minor
---

Improve docs & Refactor few hooks
5 changes: 5 additions & 0 deletions .changeset/tidy-radios-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codiume/hooks": minor
---

Improve [use-hover] & [use-clipboard] hooks
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@ A collection of reusable react hooks for state and UI management
```bash
# Using PNPM
pnpm install @codiume/hooks
```

```bash
# Using Bun
bun add @codiume/hooks
```

```bash
# Using NPM
npm install @codiume/hooks
```

```bash
# Using Yarn
yarn add @codiume/hooks
```
Expand All @@ -26,12 +35,15 @@ yarn add @codiume/hooks

| Hook | Description |
| -------------------------------------------- | ----------------------------------------------- |
| [use-clipboard](./src/use-clipboard) | Copy text to the clipboard with ease |
| [use-hover](./src/use-hover) | Tracks the hover state of a DOM element |
| [use-in-viewport](./src/use-in-viewport) | Detects if element is visible in the viewport |
| [use-local-storage](./src/use-local-storage) | Manages state with localStorage synchronization |
| [use-media-query](./src/use-media-query) | Track if a media query matches the viewport |
| [use-queue](./src/use-queue) | Manage state of elements in FIFO-like strategy |
| [use-scroll](./src/use-scroll) | Tracks scroll position of an element |
| [use-singleton](./src/use-singleton) | Creates a value exactly once |
| [use-window-scroll](./src/use-window-scroll) | Tracks scroll position of the window |
| [use-local-storage](./src/use-local-storage) | Manages state with localStorage synchronization |

## Contributing

Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export { useClipboard } from './use-clipboard/use-clipboard';
export { useHover } from './use-hover/use-hover';
export { useInViewport } from './use-in-viewport/use-in-viewport';
export { useLocalStorage } from './use-local-storage/use-local-storage';
export { useMediaQuery } from './use-media-query/use-media-query';
export { useQueue } from './use-queue/use-queue';
export { useScroll } from './use-scroll/use-scroll';
export { useSingleton } from './use-singleton/use-singleton';
export { useWindowScroll } from './use-window-scroll/use-window-scroll';
export { useMediaQuery } from './use-media-query/use-media-query';
export { useClipboard } from './use-clip-board/use-clip-board';
export { useHover } from './use-hover/use-hover';
30 changes: 0 additions & 30 deletions src/use-clip-board/README.md

This file was deleted.

26 changes: 26 additions & 0 deletions src/use-clipboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ⌨ useClipboard

A React hook that simplifies copying text to the clipboard. It offers an easy-to-use API for copying text and verifying if the operation was successful.

## Usage

```jsx
import React from 'react';
import { useClipboard } from '@codiume/hooks';

export default function App() {
const { copy, copied } = useClipboard();

const handleCopy = () => {
copy('Hello, world!');
};

return (
<div>
<button onClick={handleCopy}>
{copied ? 'Copied!' : 'Copy to Clipboard'}
</button>
</div>
);
}
```
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { useClipboard } from './use-clip-board';
import { useClipboard } from './use-clipboard';

vi.useFakeTimers();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useState } from 'react';
import { rescue } from '../utils';

export function useClipboard() {
const [copied, setCopied] = useState(false);
Expand All @@ -8,16 +9,18 @@ export function useClipboard() {
console.error('Clipboard API not supported');
return false;
}

try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
return true;
} catch (error) {
console.error('Failed to copy text:', error);
return false;
}
return rescue<boolean>(
async () => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
return true;
},
(err) => {
console.error('Failed to copy text:', err);
return false;
}
);
}, []);

return { copy, copied };
Expand Down
44 changes: 20 additions & 24 deletions src/use-hover/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
# useHover Hook
# 🖱️ useHover

A custom React hook for hovering state on a DOM element.

## Installation

No additional installation is required. Just copy the useHover function into your project.
A React hook that tracks the hover state of a DOM element, utilizing `AbortController` for efficient cleanup.

## Usage

Expand All @@ -13,23 +9,23 @@ import { useRef } from 'react';
import { useHover } from '@codiume/hooks';

export default function App() {
const hoverRef = useRef<HTMLDivElement>(null);
const { isHovered } = useHover(hoverRef);
const hoverRef = useRef<HTMLDivElement>(null);
const { isHovered } = useHover(hoverRef);

return (
<div>
<div
ref={hoverRef}
style={{
width: "200px",
height: "100px",
backgroundColor: isHovered ? "blue" : "gray",
}}
>
Hover over me!
</div>
<p>{isHovered ? "You are hovering!" : "Not hovering"}</p>
</div>
);
return (
<div>
<div
ref={hoverRef}
style={{
width: "200px",
height: "100px",
backgroundColor: isHovered ? "blue" : "gray",
}}
>
Hover over me!
</div>
<p>{isHovered ? "You are hovering!" : "Not hovering"}</p>
</div>
);
}
```
```
28 changes: 16 additions & 12 deletions src/use-hover/use-hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,35 @@ describe('useHover', () => {
expect(result.current.isHovered).toBe(false);
});

it('should clean up event listeners on unmount', () => {
it('should clean up event listeners on unmount using abort signal', () => {
const ref = { current: document.createElement('div') };
const addEventListenerSpy = vi.spyOn(ref.current, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(ref.current, 'removeEventListener');
const controllerSpy = vi.spyOn(AbortController.prototype, 'abort');

const { unmount } = renderHook(() => useHover(ref));

expect(addEventListenerSpy).toHaveBeenCalledWith(
'mouseenter',
expect.any(Function)
expect.any(Function),
expect.objectContaining({
passive: true,
signal: expect.any(AbortSignal)
})
);

expect(addEventListenerSpy).toHaveBeenCalledWith(
'mouseleave',
expect.any(Function)
expect.any(Function),
expect.objectContaining({
passive: true,
signal: expect.any(AbortSignal)
})
);

unmount();

expect(removeEventListenerSpy).toHaveBeenCalledWith(
'mouseenter',
expect.any(Function)
);
expect(removeEventListenerSpy).toHaveBeenCalledWith(
'mouseleave',
expect.any(Function)
);
expect(controllerSpy).toHaveBeenCalledTimes(1);

controllerSpy.mockRestore();
});
});
17 changes: 11 additions & 6 deletions src/use-hover/use-hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ export function useHover<T extends HTMLElement | null>(ref: RefObject<T>) {
const element = ref.current;
if (!element) return;

const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
const abortController = new AbortController();

element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
element.addEventListener('mouseenter', () => setIsHovered(true), {
passive: true,
signal: abortController.signal
});

element.addEventListener('mouseleave', () => setIsHovered(false), {
passive: true,
signal: abortController.signal
});

return () => {
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
abortController.abort();
};
}, [ref]);

Expand Down
10 changes: 2 additions & 8 deletions src/use-media-query/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
# useMediaQuery

A custom React Hook for checking if a specific media query matches the current viewport.

## Installation

To use this hook, ensure you have a React project set up. Copy the `useMediaQuery` implementation into your project or include it as part of your hooks library.
A React hook that monitors whether a specific media query matches the current viewport.

## Usage

Expand All @@ -25,6 +21,4 @@ const ExampleComponent = () => {
</div>
);
};

export default ExampleComponent;
```
```
16 changes: 13 additions & 3 deletions src/use-media-query/use-media-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@ export function useMediaQuery(query: string): boolean {

useEffect(() => {
const mediaQueryList = window.matchMedia(query);
const listener = (event: MediaQueryListEvent) => setMatches(event.matches);
mediaQueryList.addEventListener('change', listener);

const abortController = new AbortController();

mediaQueryList.addEventListener(
'change',
(event: MediaQueryListEvent) => setMatches(event.matches),
{
passive: true,
signal: abortController.signal
}
);

return () => {
mediaQueryList.removeEventListener('change', listener);
abortController.abort();
};
}, [query]);

Expand Down
28 changes: 22 additions & 6 deletions src/use-scroll/use-scroll.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,6 @@ describe('useScroll', () => {
);
});

it('should clean up event listeners on unmount', () => {
const { unmount } = renderHook(() => useScroll(ref));
unmount();
expect(mockElement.removeEventListener).not.toHaveBeenCalled(); // AbortController is used instead
});

it('should provide a scrollTo function', () => {
const { result } = renderHook(() => useScroll(ref));
const [, scrollTo] = result.current;
Expand Down Expand Up @@ -121,4 +115,26 @@ describe('useScroll', () => {
behavior: 'auto'
});
});

it('should clean up event listeners on unmount using abort signal', () => {
const addEventListenerSpy = vi.spyOn(ref.current, 'addEventListener');
const controllerSpy = vi.spyOn(AbortController.prototype, 'abort');

const { unmount } = renderHook(() => useScroll(ref));

expect(addEventListenerSpy).toHaveBeenCalledWith(
'scroll',
expect.any(Function),
expect.objectContaining({
passive: true,
signal: expect.any(AbortSignal)
})
);

unmount();

expect(controllerSpy).toHaveBeenCalledTimes(1);

controllerSpy.mockRestore();
});
});
4 changes: 2 additions & 2 deletions src/utils/rescue.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
type CallbackFunc<T> = () => T;
type CallbackFunc<T> = () => T | Promise<T>;
type FallbackFunc<T> = (err: Error) => T;

export function rescue<T>(
callback: CallbackFunc<T>,
fallback: FallbackFunc<T> = (err) => null as unknown as T
): T {
): T | Promise<T> {
try {
return callback();
} catch (err) {
Expand Down
Loading