# 📦 hookify-react

🚀 A collection of **high-performance, reusable, and production-ready React hooks** to simplify state management, DOM interaction, location, async management and browser storage.

![GitHub package.json version](https://img.shields.io/github/package-json/v/uttamakwana/hookify-react)
![npm](https://img.shields.io/npm/dt/hookify-react)
![GitHub stars](https://img.shields.io/github/stars/uttamakwana/hookify-react?style=social)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
![GitHub issues](https://img.shields.io/github/issues/uttamakwana/hookify-react)

## 🌟 Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Available Hooks](#available-hooks)
- [API Reference](#api-reference)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)

## <a id="features"></a> 🚀 Features

- ✅ Fully typed with TypeScript
- ✅ **SSR-safe** — works with Next.js, Remix and other server-rendered frameworks
- ✅ No external dependencies (only `react` as a peer dependency)
- ✅ Tree-shakeable ESM + CommonJS builds
- ✅ Stable function identities (safe for dependency arrays and memoized children)
- ✅ Automatic listener/timer cleanup — no leaks on unmount
- ✅ Unit-tested with Vitest + Testing Library

---

## <a id="installation"></a> 📌 Installation

```sh
npm install hookify-react
```

or

```sh
yarn add hookify-react
```

> **Peer dependencies:** `react >= 18` and `react-dom >= 18`.

## <a id="quick-start"></a> ⚡ Quick Start

```tsx
import { useRef } from "react";
import { useEventListener } from "hookify-react";

export default function Example() {
  const buttonRef = useRef<HTMLButtonElement>(null);

  useEventListener("click", () => alert("Button clicked!"), buttonRef);

  return <button ref={buttonRef}>Click Me</button>;
}
```

## <a id="available-hooks"></a> 🔍 Available Hooks

| Category              | Hooks                                                                                                                                  |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| **Async Management**  | `useDebounce`, `useInterval`, `useTimeout`                                                                                             |
| **Effects**           | `useAdvancedEffect`, `useUpdatedEffect`                                                                                                |
| **DOM Interactions**  | `useCopyToClipboard`, `useEventListener`, `useHover`, `useClickOutside`, `useOnlineStatus`, `useOnScreen`, `usePress`, `useScrollInfo` |
| **Responsive Design** | `useSize`, `useWindowSize`                                                                                                             |
| **State Management**  | `useArray`, `useCounter`, `useFormState`, `useHistory`, `usePrevious`, `useToggle`                                                     |
| **Storage**           | `useStorage`, `useLocalStorage`, `useSessionStorage`                                                                                   |
| **Location**          | `useGeoLocation`                                                                                                                       |

## <a id="api-reference"></a> 📖 API Reference

### Async Management

```ts
// Runs `callback` once the dependencies have been stable for `delay` ms.
useDebounce(callback: () => void, delay: number, deps: unknown[]): void

// Auto-starts on mount; pass `null` to pause. Returns a manual stopper.
useInterval(callback: () => void, interval?: number | null): { clear: () => void }

// Auto-starts on mount. Always uses the latest callback (no need to memoize).
useTimeout(callback: () => void, delay: number): {
  set: () => void;
  clear: () => void;
  reset: () => void;
}
```

### Effects

```ts
// Like useEffect, but skips the very first render (runs only on updates).
useUpdatedEffect(effect: EffectCallback, deps: DependencyList): void

// Skips the first render and runs only when deps actually change (Object.is).
useAdvancedEffect(effect: EffectCallback, deps: DependencyList): void
```

### DOM Interactions

```ts
// Uses the async Clipboard API with an execCommand fallback for insecure contexts.
useCopyToClipboard(resetDelay?: number): {
  copy: (text: string) => Promise<boolean>; // resolves to `true` on success
  isCopied: boolean;
  error: Error | null;
}

// Defaults to `window`; overloads also accept a Document or HTMLElement ref.
useEventListener(eventType, callback, elementRef?, options?): void

useHover<T extends HTMLElement>(): { ref: Ref<T>; isHovered: boolean }

useClickOutside<T extends HTMLElement>(
  callback: (event: MouseEvent | TouchEvent) => void,
): { ref: Ref<T> }

// Listens to both `online` and `offline`; SSR-safe (assumes online on the server).
useOnlineStatus(): { isOnline: boolean; onlineStatus: "online" | "offline" }

useOnScreen<T extends HTMLElement>(
  options?: string | { rootMargin?: string; threshold?: number | number[]; once?: boolean },
): { ref: Ref<T>; isVisible: boolean }

usePress<T extends HTMLElement>(): { ref: Ref<T>; isPressed: boolean }

useScrollInfo<T extends HTMLElement>(): {
  ref: Ref<T>;
  scrollX: number;
  scrollY: number;
  scrollDirection: "up" | "down" | "left" | "right" | "none";
  isScrolling: boolean;
  scrollProgress: number; // 0–100
}
```

### Responsive Design

```ts
useSize<T extends HTMLElement>(): {
  ref: Ref<T>;
  size: { width; height; top; left; bottom; right } | null;
}

useWindowSize(): { width: number; height: number }
```

### State Management

```ts
// `initialValue` defaults to []. All helper methods are referentially stable.
useArray<T>(initialValue?: T[]): [T[], SetState<T[]>, {
  push; pop; shift; unshift; removeByIndex; removeByValue;
  clear; replace; reset; filter; updateByIndex; updateByValue;
}]

// Optional { min, max } bounds clamp every update.
useCounter(initialValue?: number, options?: { min?: number; max?: number }): {
  count; increment; incrementByValue; decrement; decrementByValue; set; reset;
}

useFormState<T>(defaultValue, predicates, options?): [
  T,
  (value: T | ((prev: T) => T)) => void,
  { errors: string[]; isValid: boolean; status: "idle" | "valid" | "error" },
]

useHistory<T>(defaultValue, options?): [
  T,
  (value: T | ((prev: T) => T)) => void,
  { history: T[]; pointer: number; back; forward; go },
]

usePrevious<T>(value: T): T | null

// Call with no argument to flip, or a boolean to force a value.
useToggle(initialValue?: boolean): [boolean, (value?: boolean) => void]
```

### Storage

All storage hooks are SSR-safe and fall back to the default value on the server.
`useLocalStorage` additionally syncs across browser tabs via the `storage` event.

```ts
useLocalStorage<T>(key: string, defaultValue: T | (() => T)): [T, SetState<T>]
useSessionStorage<T>(key: string, defaultValue: T | (() => T)): [T, SetState<T>]
useStorage<T>(key: string, defaultValue: T | (() => T), type: "local" | "session"): [T, SetState<T>]
```

### Location

```ts
// Options are read by value, so passing an inline object every render is safe.
useGeoLocation(options?: {
  enableHighAccuracy?: boolean;
  maximumAge?: number;
  timeout?: number;
  retryLimit?: number;
  retryDelay?: number;
}): { loading: boolean; error: { code: number; message: string } | null; coords: GeolocationCoordinates | null }
```

## ⚠️ Migration notes (since `0.0.5`)

- **`useCopyToClipboard`** — `error` is now an `Error | null` (previously a `string`). Render `error.message`. `copy()` now resolves to a `boolean`.
- **`useOnlineStatus`** — now also exposes `isOnline`. The existing `onlineStatus` field is unchanged.
- **`useStorage`** — the third argument is now `"local" | "session"` instead of a `Storage` object. `useLocalStorage`/`useSessionStorage` are unchanged.

## <a id="contributing"></a> 🤝 Contributing

We welcome contributions! If you have suggestions for improvements or new hooks, please open an issue or submit a pull request.

```sh
npm install      # install dependencies
npm run test     # run the Vitest suite
npm run lint     # run ESLint
npm run build    # build the package
```

1. Fork the repository.
2. Create your feature branch (`git checkout -b feature/AmazingFeature`).
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`).
4. Push to the branch (`git push origin feature/AmazingFeature`).
5. Open a pull request.

## <a id="license"></a> 📜 License

This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.

## <a id="contact"></a> 📬 Contact

For any inquiries or support, please reach out to uttamakwana4503@gmail.com.

## Documentation

Visit [hookify-react](https://hookify-react.netlify.app) for full documentation.
