usehookify 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vivek Kumar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # usehookify
2
+
3
+ [![npm version](https://img.shields.io/npm/v/usehookify.svg)](https://www.npmjs.com/package/usehookify)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vivekKumarSinghH/usehook-js/blob/main/LICENSE)
5
+
6
+ A small, dependency-free React hooks library you can install as an npm
7
+ package or copy directly into your project via a CLI — same maintained
8
+ source either way.
9
+
10
+ ## Hooks
11
+
12
+ | Hook | Category | Description |
13
+ |---|---|---|
14
+ | `useLocalStorage` | storage | Persist React state to the browser's localStorage, synced across re-renders. |
15
+ | `useFetch` | network | Fetch data from a URL with loading/error state and a manual refetch. |
16
+ | `useDebounce` | utility | Return a debounced version of a rapidly-changing value. |
17
+ | `useToggle` | state | Manage a boolean value with a toggle function and an explicit setter. |
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install usehookify
23
+ ```
24
+
25
+ ```ts
26
+ import { useLocalStorage } from 'usehookify'
27
+ ```
28
+
29
+ ## Or copy it directly — same source, no dependency
30
+
31
+ ```bash
32
+ npx usehookify add useDebounce
33
+ ```
34
+
35
+ ```ts
36
+ import { useDebounce } from './hooks/useDebounce'
37
+ ```
38
+
39
+ Both paths ship the exact same, byte-identical source.
40
+
41
+ ## CLI
42
+
43
+ ```bash
44
+ npx usehookify list
45
+ ```
46
+
47
+ ## Documentation
48
+
49
+ Full API reference, parameters, and usage examples:
50
+ [github.com/vivekKumarSinghH/usehook-js](https://github.com/vivekKumarSinghH/usehook-js)
51
+
52
+ ## License
53
+
54
+ MIT © Vivek Kumar
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/bin.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/registry/hooks.ts
7
+ var hooks = [
8
+ {
9
+ id: "useLocalStorage",
10
+ name: "useLocalStorage",
11
+ description: "Persist React state to the browser's localStorage, synced across re-renders.",
12
+ category: "storage",
13
+ sourceFile: "useLocalStorage.ts",
14
+ params: [
15
+ { name: "key", type: "string", description: "The localStorage key to read/write.", required: true },
16
+ {
17
+ name: "initialValue",
18
+ type: "T",
19
+ description: "Value used when nothing is stored yet, or on the server during SSR.",
20
+ required: true
21
+ }
22
+ ],
23
+ returns: "[T, (value: T | ((prev: T) => T)) => void]",
24
+ examples: [
25
+ {
26
+ title: "Basic usage",
27
+ code: "const [name, setName] = useLocalStorage('name', 'Anonymous')\n\nsetName('Ada')\nsetName((prev) => prev.toUpperCase())"
28
+ }
29
+ ]
30
+ },
31
+ {
32
+ id: "useFetch",
33
+ name: "useFetch",
34
+ description: "Fetch data from a URL with loading/error state and a manual refetch.",
35
+ category: "network",
36
+ sourceFile: "useFetch.ts",
37
+ params: [
38
+ { name: "url", type: "string", description: "The URL to fetch.", required: true },
39
+ {
40
+ name: "options",
41
+ type: "RequestInit",
42
+ description: "Standard fetch options (headers, method, body, etc.).",
43
+ required: false
44
+ }
45
+ ],
46
+ returns: "{ data: T | null, error: Error | null, loading: boolean, refetch: () => void }",
47
+ examples: [
48
+ {
49
+ title: "Basic usage",
50
+ code: "const { data, error, loading, refetch } = useFetch<Post[]>('/api/posts')\n\nif (loading) return <Spinner />\nif (error) return <ErrorBanner message={error.message} onRetry={refetch} />\nreturn <PostList posts={data} />"
51
+ }
52
+ ]
53
+ },
54
+ {
55
+ id: "useDebounce",
56
+ name: "useDebounce",
57
+ description: "Return a debounced version of a rapidly-changing value.",
58
+ category: "utility",
59
+ sourceFile: "useDebounce.ts",
60
+ params: [
61
+ { name: "value", type: "T", description: "The value to debounce.", required: true },
62
+ { name: "delayMs", type: "number", description: "Delay in milliseconds.", required: true }
63
+ ],
64
+ returns: "T",
65
+ examples: [
66
+ {
67
+ title: "Debounced search input",
68
+ code: "const [query, setQuery] = useState('')\nconst debouncedQuery = useDebounce(query, 300)\n\n// debouncedQuery only updates 300ms after the user stops typing"
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ id: "useToggle",
74
+ name: "useToggle",
75
+ description: "Manage a boolean value with a toggle function and an explicit setter.",
76
+ category: "state",
77
+ sourceFile: "useToggle.ts",
78
+ params: [
79
+ {
80
+ name: "initial",
81
+ type: "boolean",
82
+ description: "Starting value. Defaults to false.",
83
+ required: false
84
+ }
85
+ ],
86
+ returns: "[boolean, () => void, (value: boolean) => void]",
87
+ examples: [
88
+ {
89
+ title: "Modal open/close state",
90
+ code: 'const [isOpen, toggleOpen, setOpen] = useToggle(false)\n\n// flip on button click:\ntoggleOpen()\n\n// force closed, e.g. from an "X" button \u2014 never toggles back open:\nsetOpen(false)'
91
+ }
92
+ ]
93
+ }
94
+ ];
95
+
96
+ // src/cli/commands/list.ts
97
+ function printHookList(hookList = hooks) {
98
+ if (hookList.length === 0) {
99
+ console.log("No hooks available yet.");
100
+ return;
101
+ }
102
+ for (const hook of hookList) {
103
+ console.log(`${hook.id} ${hook.name} ${hook.category} ${hook.description}`);
104
+ }
105
+ }
106
+ function registerListCommand(program2) {
107
+ program2.command("list").description("List every hook available in usehookify").action(() => printHookList());
108
+ }
109
+
110
+ // src/cli/commands/add.ts
111
+ import path2 from "path";
112
+
113
+ // src/infra/fileWriter.ts
114
+ import { existsSync, mkdirSync, copyFileSync } from "fs";
115
+ import { fileURLToPath } from "url";
116
+ import path from "path";
117
+
118
+ // src/cli/errors.ts
119
+ var InvalidHookIdError = class extends Error {
120
+ constructor(hookId, validIds) {
121
+ super(`Unknown hook "${hookId}". Valid hooks: ${validIds.join(", ")}`);
122
+ this.hookId = hookId;
123
+ this.validIds = validIds;
124
+ }
125
+ };
126
+ var TargetExistsError = class extends Error {
127
+ constructor(path3) {
128
+ super(`"${path3}" already exists. Re-run with --force to overwrite.`);
129
+ this.path = path3;
130
+ }
131
+ };
132
+
133
+ // src/infra/fileWriter.ts
134
+ function getHooksSourceDir() {
135
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
136
+ return path.join(packageRoot, "src", "hooks");
137
+ }
138
+ function writeHookFile(sourceFile, targetDir, options = {}) {
139
+ const sourcePath = path.join(getHooksSourceDir(), sourceFile);
140
+ const destPath = path.join(targetDir, sourceFile);
141
+ if (existsSync(destPath) && !options.force) {
142
+ throw new TargetExistsError(destPath);
143
+ }
144
+ mkdirSync(targetDir, { recursive: true });
145
+ copyFileSync(sourcePath, destPath);
146
+ return destPath;
147
+ }
148
+
149
+ // src/cli/commands/add.ts
150
+ function resolveHook(hookId, hookList = hooks) {
151
+ const hook = hookList.find((h) => h.id === hookId);
152
+ if (!hook) {
153
+ throw new InvalidHookIdError(
154
+ hookId,
155
+ hookList.map((h) => h.id)
156
+ );
157
+ }
158
+ return hook;
159
+ }
160
+ function runAdd(hookId, options) {
161
+ const hook = resolveHook(hookId);
162
+ const targetDir = options.path ?? "./hooks";
163
+ const writtenPath = writeHookFile(hook.sourceFile, targetDir, { force: options.force });
164
+ const relativeFromCwd = path2.relative(process.cwd(), path2.resolve(targetDir));
165
+ const normalizedDir = relativeFromCwd.split(path2.sep).join("/");
166
+ const importDir = normalizedDir.startsWith(".") ? normalizedDir : `./${normalizedDir}`;
167
+ const importLine = `import { ${hook.name} } from '${importDir}/${hookId}'`;
168
+ return { writtenPath, importLine };
169
+ }
170
+ function registerAddCommand(program2) {
171
+ program2.command("add <hookId>").description("Copy a hook's source file into your project").option("-p, --path <dir>", "Directory to copy the hook into", "./hooks").option("-f, --force", "Overwrite the target file if it already exists", false).action((hookId, options) => {
172
+ try {
173
+ const { writtenPath, importLine } = runAdd(hookId, options);
174
+ console.log(`Copied to ${writtenPath}`);
175
+ console.log(`Add this import: ${importLine}`);
176
+ } catch (err) {
177
+ if (err instanceof InvalidHookIdError || err instanceof TargetExistsError) {
178
+ console.error(`Error: ${err.message}`);
179
+ process.exit(1);
180
+ return;
181
+ }
182
+ throw err;
183
+ }
184
+ });
185
+ }
186
+
187
+ // src/cli/bin.ts
188
+ var program = new Command();
189
+ program.name("usehookify").description("usehookify CLI \u2014 list and copy React hooks").version("1.0.0");
190
+ registerListCommand(program);
191
+ registerAddCommand(program);
192
+ program.parse(process.argv);
package/dist/index.cjs ADDED
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ useDebounce: () => useDebounce,
24
+ useFetch: () => useFetch,
25
+ useLocalStorage: () => useLocalStorage,
26
+ useToggle: () => useToggle
27
+ });
28
+ module.exports = __toCommonJS(src_exports);
29
+
30
+ // src/hooks/useLocalStorage.ts
31
+ var import_react = require("react");
32
+ function readValue(key, initialValue) {
33
+ if (typeof window === "undefined") return initialValue;
34
+ try {
35
+ const stored = window.localStorage.getItem(key);
36
+ return stored ? JSON.parse(stored) : initialValue;
37
+ } catch {
38
+ return initialValue;
39
+ }
40
+ }
41
+ function writeValue(key, value) {
42
+ if (typeof window === "undefined") return;
43
+ try {
44
+ window.localStorage.setItem(key, JSON.stringify(value));
45
+ } catch {
46
+ }
47
+ }
48
+ function useLocalStorage(key, initialValue) {
49
+ const [storedValue, setStoredValue] = (0, import_react.useState)(() => readValue(key, initialValue));
50
+ const setValue = (0, import_react.useCallback)(
51
+ (value) => {
52
+ setStoredValue((prev) => {
53
+ const next = value instanceof Function ? value(prev) : value;
54
+ writeValue(key, next);
55
+ return next;
56
+ });
57
+ },
58
+ [key]
59
+ );
60
+ return [storedValue, setValue];
61
+ }
62
+
63
+ // src/hooks/useFetch.ts
64
+ var import_react2 = require("react");
65
+ function useFetch(url, options) {
66
+ const [data, setData] = (0, import_react2.useState)(null);
67
+ const [error, setError] = (0, import_react2.useState)(null);
68
+ const [loading, setLoading] = (0, import_react2.useState)(true);
69
+ const [version, setVersion] = (0, import_react2.useState)(0);
70
+ const mountedRef = (0, import_react2.useRef)(true);
71
+ (0, import_react2.useEffect)(() => {
72
+ mountedRef.current = true;
73
+ return () => {
74
+ mountedRef.current = false;
75
+ };
76
+ }, []);
77
+ (0, import_react2.useEffect)(() => {
78
+ setLoading(true);
79
+ setError(null);
80
+ fetch(url, options).then(async (response) => {
81
+ if (!response.ok) {
82
+ throw new Error(`Request failed with status ${response.status}`);
83
+ }
84
+ return await response.json();
85
+ }).then((result) => {
86
+ if (!mountedRef.current) return;
87
+ setData(result);
88
+ setLoading(false);
89
+ }).catch((err) => {
90
+ if (!mountedRef.current) return;
91
+ setError(err instanceof Error ? err : new Error(String(err)));
92
+ setLoading(false);
93
+ });
94
+ }, [url, version]);
95
+ const refetch = (0, import_react2.useCallback)(() => {
96
+ setVersion((v) => v + 1);
97
+ }, []);
98
+ return { data, error, loading, refetch };
99
+ }
100
+
101
+ // src/hooks/useDebounce.ts
102
+ var import_react3 = require("react");
103
+ function useDebounce(value, delayMs) {
104
+ const [debouncedValue, setDebouncedValue] = (0, import_react3.useState)(value);
105
+ (0, import_react3.useEffect)(() => {
106
+ const timer = setTimeout(() => {
107
+ setDebouncedValue(value);
108
+ }, delayMs);
109
+ return () => clearTimeout(timer);
110
+ }, [value, delayMs]);
111
+ return debouncedValue;
112
+ }
113
+
114
+ // src/hooks/useToggle.ts
115
+ var import_react4 = require("react");
116
+ function useToggle(initial = false) {
117
+ const [value, setValue] = (0, import_react4.useState)(initial);
118
+ const toggle = (0, import_react4.useCallback)(() => {
119
+ setValue((prev) => !prev);
120
+ }, []);
121
+ const setExplicit = (0, import_react4.useCallback)((next) => {
122
+ setValue(next);
123
+ }, []);
124
+ return [value, toggle, setExplicit];
125
+ }
126
+ // Annotate the CommonJS export names for ESM import in node:
127
+ 0 && (module.exports = {
128
+ useDebounce,
129
+ useFetch,
130
+ useLocalStorage,
131
+ useToggle
132
+ });
@@ -0,0 +1,16 @@
1
+ type SetValue<T> = T | ((prev: T) => T);
2
+ declare function useLocalStorage<T>(key: string, initialValue: T): [T, (value: SetValue<T>) => void];
3
+
4
+ interface UseFetchResult<T> {
5
+ data: T | null;
6
+ error: Error | null;
7
+ loading: boolean;
8
+ refetch: () => void;
9
+ }
10
+ declare function useFetch<T>(url: string, options?: RequestInit): UseFetchResult<T>;
11
+
12
+ declare function useDebounce<T>(value: T, delayMs: number): T;
13
+
14
+ declare function useToggle(initial?: boolean): [boolean, () => void, (value: boolean) => void];
15
+
16
+ export { useDebounce, useFetch, useLocalStorage, useToggle };
@@ -0,0 +1,16 @@
1
+ type SetValue<T> = T | ((prev: T) => T);
2
+ declare function useLocalStorage<T>(key: string, initialValue: T): [T, (value: SetValue<T>) => void];
3
+
4
+ interface UseFetchResult<T> {
5
+ data: T | null;
6
+ error: Error | null;
7
+ loading: boolean;
8
+ refetch: () => void;
9
+ }
10
+ declare function useFetch<T>(url: string, options?: RequestInit): UseFetchResult<T>;
11
+
12
+ declare function useDebounce<T>(value: T, delayMs: number): T;
13
+
14
+ declare function useToggle(initial?: boolean): [boolean, () => void, (value: boolean) => void];
15
+
16
+ export { useDebounce, useFetch, useLocalStorage, useToggle };
package/dist/index.js ADDED
@@ -0,0 +1,102 @@
1
+ // src/hooks/useLocalStorage.ts
2
+ import { useCallback, useState } from "react";
3
+ function readValue(key, initialValue) {
4
+ if (typeof window === "undefined") return initialValue;
5
+ try {
6
+ const stored = window.localStorage.getItem(key);
7
+ return stored ? JSON.parse(stored) : initialValue;
8
+ } catch {
9
+ return initialValue;
10
+ }
11
+ }
12
+ function writeValue(key, value) {
13
+ if (typeof window === "undefined") return;
14
+ try {
15
+ window.localStorage.setItem(key, JSON.stringify(value));
16
+ } catch {
17
+ }
18
+ }
19
+ function useLocalStorage(key, initialValue) {
20
+ const [storedValue, setStoredValue] = useState(() => readValue(key, initialValue));
21
+ const setValue = useCallback(
22
+ (value) => {
23
+ setStoredValue((prev) => {
24
+ const next = value instanceof Function ? value(prev) : value;
25
+ writeValue(key, next);
26
+ return next;
27
+ });
28
+ },
29
+ [key]
30
+ );
31
+ return [storedValue, setValue];
32
+ }
33
+
34
+ // src/hooks/useFetch.ts
35
+ import { useCallback as useCallback2, useEffect, useRef, useState as useState2 } from "react";
36
+ function useFetch(url, options) {
37
+ const [data, setData] = useState2(null);
38
+ const [error, setError] = useState2(null);
39
+ const [loading, setLoading] = useState2(true);
40
+ const [version, setVersion] = useState2(0);
41
+ const mountedRef = useRef(true);
42
+ useEffect(() => {
43
+ mountedRef.current = true;
44
+ return () => {
45
+ mountedRef.current = false;
46
+ };
47
+ }, []);
48
+ useEffect(() => {
49
+ setLoading(true);
50
+ setError(null);
51
+ fetch(url, options).then(async (response) => {
52
+ if (!response.ok) {
53
+ throw new Error(`Request failed with status ${response.status}`);
54
+ }
55
+ return await response.json();
56
+ }).then((result) => {
57
+ if (!mountedRef.current) return;
58
+ setData(result);
59
+ setLoading(false);
60
+ }).catch((err) => {
61
+ if (!mountedRef.current) return;
62
+ setError(err instanceof Error ? err : new Error(String(err)));
63
+ setLoading(false);
64
+ });
65
+ }, [url, version]);
66
+ const refetch = useCallback2(() => {
67
+ setVersion((v) => v + 1);
68
+ }, []);
69
+ return { data, error, loading, refetch };
70
+ }
71
+
72
+ // src/hooks/useDebounce.ts
73
+ import { useEffect as useEffect2, useState as useState3 } from "react";
74
+ function useDebounce(value, delayMs) {
75
+ const [debouncedValue, setDebouncedValue] = useState3(value);
76
+ useEffect2(() => {
77
+ const timer = setTimeout(() => {
78
+ setDebouncedValue(value);
79
+ }, delayMs);
80
+ return () => clearTimeout(timer);
81
+ }, [value, delayMs]);
82
+ return debouncedValue;
83
+ }
84
+
85
+ // src/hooks/useToggle.ts
86
+ import { useCallback as useCallback3, useState as useState4 } from "react";
87
+ function useToggle(initial = false) {
88
+ const [value, setValue] = useState4(initial);
89
+ const toggle = useCallback3(() => {
90
+ setValue((prev) => !prev);
91
+ }, []);
92
+ const setExplicit = useCallback3((next) => {
93
+ setValue(next);
94
+ }, []);
95
+ return [value, toggle, setExplicit];
96
+ }
97
+ export {
98
+ useDebounce,
99
+ useFetch,
100
+ useLocalStorage,
101
+ useToggle
102
+ };
@@ -0,0 +1,24 @@
1
+ interface Param {
2
+ name: string;
3
+ type: string;
4
+ description: string;
5
+ required: boolean;
6
+ }
7
+ interface Example {
8
+ title: string;
9
+ code: string;
10
+ }
11
+ interface HookMetadata {
12
+ id: string;
13
+ name: string;
14
+ description: string;
15
+ category: string;
16
+ sourceFile: string;
17
+ params: Param[];
18
+ returns: string;
19
+ examples: Example[];
20
+ }
21
+
22
+ declare const hooks: HookMetadata[];
23
+
24
+ export { type Example, type HookMetadata, type Param, hooks };
@@ -0,0 +1,92 @@
1
+ // src/registry/hooks.ts
2
+ var hooks = [
3
+ {
4
+ id: "useLocalStorage",
5
+ name: "useLocalStorage",
6
+ description: "Persist React state to the browser's localStorage, synced across re-renders.",
7
+ category: "storage",
8
+ sourceFile: "useLocalStorage.ts",
9
+ params: [
10
+ { name: "key", type: "string", description: "The localStorage key to read/write.", required: true },
11
+ {
12
+ name: "initialValue",
13
+ type: "T",
14
+ description: "Value used when nothing is stored yet, or on the server during SSR.",
15
+ required: true
16
+ }
17
+ ],
18
+ returns: "[T, (value: T | ((prev: T) => T)) => void]",
19
+ examples: [
20
+ {
21
+ title: "Basic usage",
22
+ code: "const [name, setName] = useLocalStorage('name', 'Anonymous')\n\nsetName('Ada')\nsetName((prev) => prev.toUpperCase())"
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ id: "useFetch",
28
+ name: "useFetch",
29
+ description: "Fetch data from a URL with loading/error state and a manual refetch.",
30
+ category: "network",
31
+ sourceFile: "useFetch.ts",
32
+ params: [
33
+ { name: "url", type: "string", description: "The URL to fetch.", required: true },
34
+ {
35
+ name: "options",
36
+ type: "RequestInit",
37
+ description: "Standard fetch options (headers, method, body, etc.).",
38
+ required: false
39
+ }
40
+ ],
41
+ returns: "{ data: T | null, error: Error | null, loading: boolean, refetch: () => void }",
42
+ examples: [
43
+ {
44
+ title: "Basic usage",
45
+ code: "const { data, error, loading, refetch } = useFetch<Post[]>('/api/posts')\n\nif (loading) return <Spinner />\nif (error) return <ErrorBanner message={error.message} onRetry={refetch} />\nreturn <PostList posts={data} />"
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ id: "useDebounce",
51
+ name: "useDebounce",
52
+ description: "Return a debounced version of a rapidly-changing value.",
53
+ category: "utility",
54
+ sourceFile: "useDebounce.ts",
55
+ params: [
56
+ { name: "value", type: "T", description: "The value to debounce.", required: true },
57
+ { name: "delayMs", type: "number", description: "Delay in milliseconds.", required: true }
58
+ ],
59
+ returns: "T",
60
+ examples: [
61
+ {
62
+ title: "Debounced search input",
63
+ code: "const [query, setQuery] = useState('')\nconst debouncedQuery = useDebounce(query, 300)\n\n// debouncedQuery only updates 300ms after the user stops typing"
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ id: "useToggle",
69
+ name: "useToggle",
70
+ description: "Manage a boolean value with a toggle function and an explicit setter.",
71
+ category: "state",
72
+ sourceFile: "useToggle.ts",
73
+ params: [
74
+ {
75
+ name: "initial",
76
+ type: "boolean",
77
+ description: "Starting value. Defaults to false.",
78
+ required: false
79
+ }
80
+ ],
81
+ returns: "[boolean, () => void, (value: boolean) => void]",
82
+ examples: [
83
+ {
84
+ title: "Modal open/close state",
85
+ code: 'const [isOpen, toggleOpen, setOpen] = useToggle(false)\n\n// flip on button click:\ntoggleOpen()\n\n// force closed, e.g. from an "X" button \u2014 never toggles back open:\nsetOpen(false)'
86
+ }
87
+ ]
88
+ }
89
+ ];
90
+ export {
91
+ hooks
92
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "usehookify",
3
+ "version": "1.0.0",
4
+ "description": "A small, dependency-free React hooks library — install as a package or copy via CLI, same source either way.",
5
+ "license": "MIT",
6
+ "author": "Vivek Kumar",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/vivekKumarSinghH/usehook-js.git",
10
+ "directory": "packages/usehook-js"
11
+ },
12
+ "keywords": ["react", "hooks", "react-hooks", "typescript", "cli"],
13
+ "type": "module",
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "require": {
24
+ "types": "./dist/index.d.cts",
25
+ "default": "./dist/index.cjs"
26
+ }
27
+ },
28
+ "./registry": {
29
+ "types": "./dist/registry/index.d.ts",
30
+ "default": "./dist/registry/index.js"
31
+ }
32
+ },
33
+ "files": ["dist", "src/hooks"],
34
+ "bin": {
35
+ "usehookify": "./dist/cli/bin.js"
36
+ },
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "test": "vitest run --coverage",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint src --max-warnings=0"
42
+ },
43
+ "peerDependencies": {
44
+ "react": ">=16.8.0"
45
+ },
46
+ "dependencies": {
47
+ "commander": "^12.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "tsup": "^8.0.0",
51
+ "vitest": "^2.0.0",
52
+ "@vitest/coverage-v8": "^2.0.0",
53
+ "jsdom": "^25.0.0",
54
+ "@testing-library/react": "^16.0.0",
55
+ "react": "^18.0.0",
56
+ "react-dom": "^18.0.0",
57
+ "@types/react": "^18.0.0",
58
+ "@types/node": "^22.0.0",
59
+ "typescript": "^5.5.0"
60
+ }
61
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { useDebounce } from './useDebounce'
4
+
5
+ describe('useDebounce', () => {
6
+ afterEach(() => {
7
+ vi.useRealTimers()
8
+ })
9
+
10
+ it('returns the initial value immediately', () => {
11
+ const { result } = renderHook(() => useDebounce('a', 300))
12
+ expect(result.current).toBe('a')
13
+ })
14
+
15
+ it('returns the latest value only after the delay elapses', () => {
16
+ vi.useFakeTimers()
17
+ const { result, rerender } = renderHook(({ value }) => useDebounce(value, 300), {
18
+ initialProps: { value: 'a' },
19
+ })
20
+ rerender({ value: 'b' })
21
+ act(() => vi.advanceTimersByTime(299))
22
+ expect(result.current).toBe('a') // not yet
23
+ act(() => vi.advanceTimersByTime(1))
24
+ expect(result.current).toBe('b')
25
+ })
26
+
27
+ it('only keeps the last value when changed rapidly within the delay window', () => {
28
+ vi.useFakeTimers()
29
+ const { result, rerender } = renderHook(({ value }) => useDebounce(value, 300), {
30
+ initialProps: { value: 'a' },
31
+ })
32
+ rerender({ value: 'b' })
33
+ act(() => vi.advanceTimersByTime(150))
34
+ rerender({ value: 'c' }) // resets the timer before 'b' ever fires
35
+ act(() => vi.advanceTimersByTime(150))
36
+ expect(result.current).toBe('a') // still not settled — only 150ms since 'c'
37
+ act(() => vi.advanceTimersByTime(150))
38
+ expect(result.current).toBe('c') // 'b' was skipped entirely
39
+ })
40
+
41
+ it('does not update after unmounting mid-delay', () => {
42
+ vi.useFakeTimers()
43
+ const { unmount } = renderHook(({ value }) => useDebounce(value, 300), {
44
+ initialProps: { value: 'a' },
45
+ })
46
+ unmount()
47
+ expect(() => act(() => vi.advanceTimersByTime(300))).not.toThrow()
48
+ })
49
+ })
@@ -0,0 +1,17 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ export function useDebounce<T>(value: T, delayMs: number): T {
6
+ const [debouncedValue, setDebouncedValue] = useState(value)
7
+
8
+ useEffect(() => {
9
+ const timer = setTimeout(() => {
10
+ setDebouncedValue(value)
11
+ }, delayMs)
12
+
13
+ return () => clearTimeout(timer)
14
+ }, [value, delayMs])
15
+
16
+ return debouncedValue
17
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest'
2
+ import { renderHook, waitFor, act } from '@testing-library/react'
3
+ import { useFetch } from './useFetch'
4
+
5
+ describe('useFetch', () => {
6
+ afterEach(() => {
7
+ vi.restoreAllMocks()
8
+ })
9
+
10
+ it('starts in a loading state with no data or error', () => {
11
+ vi.stubGlobal(
12
+ 'fetch',
13
+ vi.fn(() => new Promise(() => {})) // never resolves — hold in loading
14
+ )
15
+ const { result } = renderHook(() => useFetch('/api/thing'))
16
+ expect(result.current).toMatchObject({ data: null, error: null, loading: true })
17
+ })
18
+
19
+ it('returns parsed data on a successful response', async () => {
20
+ vi.stubGlobal(
21
+ 'fetch',
22
+ vi.fn(() =>
23
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ id: 1 }) } as Response)
24
+ )
25
+ )
26
+ const { result } = renderHook(() => useFetch<{ id: number }>('/api/thing'))
27
+ await waitFor(() => expect(result.current.loading).toBe(false))
28
+ expect(result.current.data).toEqual({ id: 1 })
29
+ expect(result.current.error).toBeNull()
30
+ })
31
+
32
+ it('surfaces a non-OK response as an error, never throwing', async () => {
33
+ vi.stubGlobal(
34
+ 'fetch',
35
+ vi.fn(() => Promise.resolve({ ok: false, status: 404 } as Response))
36
+ )
37
+ const { result } = renderHook(() => useFetch('/api/missing'))
38
+ await waitFor(() => expect(result.current.loading).toBe(false))
39
+ expect(result.current.data).toBeNull()
40
+ expect(result.current.error).toBeInstanceOf(Error)
41
+ })
42
+
43
+ it('surfaces a rejected fetch as an error, never throwing', async () => {
44
+ vi.stubGlobal(
45
+ 'fetch',
46
+ vi.fn(() => Promise.reject(new Error('network down')))
47
+ )
48
+ const { result } = renderHook(() => useFetch('/api/thing'))
49
+ await waitFor(() => expect(result.current.loading).toBe(false))
50
+ expect(result.current.error?.message).toBe('network down')
51
+ })
52
+
53
+ it('refetch() re-triggers the request and resets loading first', async () => {
54
+ const fetchMock = vi
55
+ .fn()
56
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ n: 1 }) } as Response)
57
+ .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ n: 2 }) } as Response)
58
+ vi.stubGlobal('fetch', fetchMock)
59
+
60
+ const { result } = renderHook(() => useFetch<{ n: number }>('/api/thing'))
61
+ await waitFor(() => expect(result.current.data).toEqual({ n: 1 }))
62
+
63
+ act(() => {
64
+ result.current.refetch()
65
+ })
66
+ // act() flushes the synchronous setLoading(true) before the mocked
67
+ // fetch's microtask resolves, so this is safe to assert immediately
68
+ // rather than via waitFor (which could miss a transient state that
69
+ // resolves within the same tick).
70
+ expect(result.current.loading).toBe(true)
71
+ await waitFor(() => expect(result.current.data).toEqual({ n: 2 }))
72
+ expect(fetchMock).toHaveBeenCalledTimes(2)
73
+ })
74
+
75
+ it('does not update state after unmounting mid-flight', async () => {
76
+ let resolveFetch: (value: Response) => void = () => {}
77
+ vi.stubGlobal(
78
+ 'fetch',
79
+ vi.fn(
80
+ () =>
81
+ new Promise<Response>((resolve) => {
82
+ resolveFetch = resolve
83
+ })
84
+ )
85
+ )
86
+ const { unmount } = renderHook(() => useFetch('/api/thing'))
87
+ unmount()
88
+ resolveFetch({ ok: true, json: () => Promise.resolve({}) } as Response)
89
+ // If the hook updated state on an unmounted component, React would log
90
+ // an error/warning — asserting no throw here is the meaningful check.
91
+ await new Promise((r) => setTimeout(r, 0))
92
+ })
93
+ })
@@ -0,0 +1,54 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+
5
+ export interface UseFetchResult<T> {
6
+ data: T | null
7
+ error: Error | null
8
+ loading: boolean
9
+ refetch: () => void
10
+ }
11
+
12
+ export function useFetch<T>(url: string, options?: RequestInit): UseFetchResult<T> {
13
+ const [data, setData] = useState<T | null>(null)
14
+ const [error, setError] = useState<Error | null>(null)
15
+ const [loading, setLoading] = useState(true)
16
+ const [version, setVersion] = useState(0)
17
+ const mountedRef = useRef(true)
18
+
19
+ useEffect(() => {
20
+ mountedRef.current = true
21
+ return () => {
22
+ mountedRef.current = false
23
+ }
24
+ }, [])
25
+
26
+ useEffect(() => {
27
+ setLoading(true)
28
+ setError(null)
29
+
30
+ fetch(url, options)
31
+ .then(async (response) => {
32
+ if (!response.ok) {
33
+ throw new Error(`Request failed with status ${response.status}`)
34
+ }
35
+ return (await response.json()) as T
36
+ })
37
+ .then((result) => {
38
+ if (!mountedRef.current) return
39
+ setData(result)
40
+ setLoading(false)
41
+ })
42
+ .catch((err: unknown) => {
43
+ if (!mountedRef.current) return
44
+ setError(err instanceof Error ? err : new Error(String(err)))
45
+ setLoading(false)
46
+ })
47
+ }, [url, version])
48
+
49
+ const refetch = useCallback(() => {
50
+ setVersion((v) => v + 1)
51
+ }, [])
52
+
53
+ return { data, error, loading, refetch }
54
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { useLocalStorage, readValue, writeValue } from './useLocalStorage'
4
+
5
+ describe('useLocalStorage', () => {
6
+ beforeEach(() => {
7
+ window.localStorage.clear()
8
+ })
9
+
10
+ it('returns the initial value when nothing is stored yet', () => {
11
+ const { result } = renderHook(() => useLocalStorage('count', 0))
12
+ expect(result.current[0]).toBe(0)
13
+ })
14
+
15
+ it('reads an existing value from localStorage on mount', () => {
16
+ window.localStorage.setItem('count', JSON.stringify(42))
17
+ const { result } = renderHook(() => useLocalStorage('count', 0))
18
+ expect(result.current[0]).toBe(42)
19
+ })
20
+
21
+ it('falls back to the initial value when stored JSON is corrupted', () => {
22
+ window.localStorage.setItem('count', '{not valid json')
23
+ const { result } = renderHook(() => useLocalStorage('count', 0))
24
+ expect(result.current[0]).toBe(0)
25
+ })
26
+
27
+ it('updates state and persists when called with a direct value', () => {
28
+ const { result } = renderHook(() => useLocalStorage('count', 0))
29
+ act(() => {
30
+ result.current[1](5)
31
+ })
32
+ expect(result.current[0]).toBe(5)
33
+ expect(window.localStorage.getItem('count')).toBe('5')
34
+ })
35
+
36
+ it('updates state based on the previous value when called with an updater function', () => {
37
+ const { result } = renderHook(() => useLocalStorage('count', 0))
38
+ act(() => {
39
+ result.current[1]((prev) => prev + 1)
40
+ })
41
+ act(() => {
42
+ result.current[1]((prev) => prev + 1)
43
+ })
44
+ expect(result.current[0]).toBe(2)
45
+ expect(window.localStorage.getItem('count')).toBe('2')
46
+ })
47
+
48
+ it('re-reads the persisted value on a fresh mount (round-trip)', () => {
49
+ const first = renderHook(() => useLocalStorage('count', 0))
50
+ act(() => {
51
+ first.result.current[1](7)
52
+ })
53
+ first.unmount()
54
+ const second = renderHook(() => useLocalStorage('count', 0))
55
+ expect(second.result.current[0]).toBe(7)
56
+ })
57
+
58
+ it('keeps state correct even if persisting throws (e.g. quota exceeded)', () => {
59
+ const setItemSpy = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
60
+ throw new DOMException('QuotaExceededError')
61
+ })
62
+ const { result } = renderHook(() => useLocalStorage('count', 0))
63
+ act(() => {
64
+ result.current[1](9)
65
+ })
66
+ expect(result.current[0]).toBe(9)
67
+ setItemSpy.mockRestore()
68
+ })
69
+
70
+ describe("readValue (SSR guard, tested directly — avoids fighting jsdom's global window)", () => {
71
+ afterEach(() => {
72
+ vi.unstubAllGlobals()
73
+ })
74
+
75
+ it('returns the initial value when window is undefined', () => {
76
+ vi.stubGlobal('window', undefined)
77
+ expect(readValue('count', 7)).toBe(7)
78
+ })
79
+ })
80
+
81
+ describe('writeValue (SSR guard, tested directly)', () => {
82
+ afterEach(() => {
83
+ vi.unstubAllGlobals()
84
+ })
85
+
86
+ it('is a no-op when window is undefined — never throws', () => {
87
+ vi.stubGlobal('window', undefined)
88
+ expect(() => writeValue('count', 1)).not.toThrow()
89
+ })
90
+ })
91
+ })
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useState } from 'react'
4
+
5
+ export type SetValue<T> = T | ((prev: T) => T)
6
+
7
+ export function readValue<T>(key: string, initialValue: T): T {
8
+ if (typeof window === 'undefined') return initialValue // SSR guard
9
+ try {
10
+ const stored = window.localStorage.getItem(key)
11
+ return stored ? (JSON.parse(stored) as T) : initialValue
12
+ } catch {
13
+ return initialValue // corrupt/foreign data in that key must not crash the caller
14
+ }
15
+ }
16
+
17
+ export function writeValue<T>(key: string, value: T): void {
18
+ if (typeof window === 'undefined') return // SSR guard — no-op if ever called server-side
19
+ try {
20
+ window.localStorage.setItem(key, JSON.stringify(value))
21
+ } catch {
22
+ // Quota exceeded, storage disabled, or a serialization error — in-memory
23
+ // state already updated by the caller; only persistence silently fails,
24
+ // matching this hook's contract of never throwing into the caller's
25
+ // render/update path.
26
+ }
27
+ }
28
+
29
+ export function useLocalStorage<T>(
30
+ key: string,
31
+ initialValue: T
32
+ ): [T, (value: SetValue<T>) => void] {
33
+ const [storedValue, setStoredValue] = useState<T>(() => readValue(key, initialValue))
34
+
35
+ const setValue = useCallback(
36
+ (value: SetValue<T>) => {
37
+ setStoredValue((prev) => {
38
+ const next = value instanceof Function ? value(prev) : value
39
+ writeValue(key, next)
40
+ return next
41
+ })
42
+ },
43
+ [key]
44
+ )
45
+
46
+ return [storedValue, setValue]
47
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { useToggle } from './useToggle'
4
+
5
+ describe('useToggle', () => {
6
+ it('defaults to false when no initial value is given', () => {
7
+ const { result } = renderHook(() => useToggle())
8
+ expect(result.current[0]).toBe(false)
9
+ })
10
+
11
+ it('starts at the given initial value', () => {
12
+ const { result } = renderHook(() => useToggle(true))
13
+ expect(result.current[0]).toBe(true)
14
+ })
15
+
16
+ it('flips the value when toggle is called', () => {
17
+ const { result } = renderHook(() => useToggle(false))
18
+ act(() => result.current[1]())
19
+ expect(result.current[0]).toBe(true)
20
+ act(() => result.current[1]())
21
+ expect(result.current[0]).toBe(false)
22
+ })
23
+
24
+ it('sets an explicit value regardless of the current value', () => {
25
+ const { result } = renderHook(() => useToggle(false))
26
+ act(() => result.current[2](true))
27
+ expect(result.current[0]).toBe(true)
28
+ act(() => result.current[2](true))
29
+ expect(result.current[0]).toBe(true)
30
+ act(() => result.current[2](false))
31
+ expect(result.current[0]).toBe(false)
32
+ })
33
+
34
+ it('keeps toggle and setValue referentially stable across re-renders', () => {
35
+ const { result, rerender } = renderHook(() => useToggle())
36
+ const [, firstToggle, firstSetValue] = result.current
37
+ rerender()
38
+ const [, secondToggle, secondSetValue] = result.current
39
+ expect(secondToggle).toBe(firstToggle)
40
+ expect(secondSetValue).toBe(firstSetValue)
41
+ })
42
+ })
@@ -0,0 +1,17 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useState } from 'react'
4
+
5
+ export function useToggle(initial = false): [boolean, () => void, (value: boolean) => void] {
6
+ const [value, setValue] = useState(initial)
7
+
8
+ const toggle = useCallback(() => {
9
+ setValue((prev) => !prev)
10
+ }, [])
11
+
12
+ const setExplicit = useCallback((next: boolean) => {
13
+ setValue(next)
14
+ }, [])
15
+
16
+ return [value, toggle, setExplicit]
17
+ }