@usefy/use-stack 0.19.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/README.md ADDED
@@ -0,0 +1,284 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mirunamu00/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">@usefy/use-stack</h1>
6
+
7
+ <p align="center">
8
+ <strong>A React hook for managing LIFO stack state with immutable updates</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@usefy/use-stack">
13
+ <img src="https://img.shields.io/npm/v/@usefy/use-stack.svg?style=flat-square&color=007acc" alt="npm version" />
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/@usefy/use-stack">
16
+ <img src="https://img.shields.io/npm/dm/@usefy/use-stack.svg?style=flat-square&color=007acc" alt="npm downloads" />
17
+ </a>
18
+ <a href="https://bundlephobia.com/package/@usefy/use-stack">
19
+ <img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-stack?style=flat-square&color=007acc" alt="bundle size" />
20
+ </a>
21
+ <a href="https://github.com/mirunamu00/usefy/blob/master/LICENSE">
22
+ <img src="https://img.shields.io/npm/l/@usefy/use-stack.svg?style=flat-square&color=007acc" alt="license" />
23
+ </a>
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="#installation">Installation</a> •
28
+ <a href="#quick-start">Quick Start</a> •
29
+ <a href="#api-reference">API Reference</a> •
30
+ <a href="#examples">Examples</a> •
31
+ <a href="#license">License</a>
32
+ </p>
33
+
34
+ <p align="center">
35
+ <a href="https://mirunamu00.github.io/usefy/?path=/docs/hooks-usestack--docs" target="_blank" rel="noopener noreferrer">
36
+ <strong>📚 View Storybook Demo</strong>
37
+ </a>
38
+ </p>
39
+
40
+ ---
41
+
42
+ ## Overview
43
+
44
+ `@usefy/use-stack` manages a **LIFO** (last-in, first-out) stack as React state with immutable, ergonomic updates. It is the LIFO sibling of [`@usefy/use-queue`](https://www.npmjs.com/package/@usefy/use-queue) — identical in shape and conventions, but `push` and `pop` operate on the **same** end (the top). Items are pushed onto the top and popped from the top. Every mutation produces a brand-new array (so React re-renders correctly and the previous state is never mutated), and the returned stack is typed as `readonly T[]` to steer you toward the provided actions.
45
+
46
+ **Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
47
+
48
+ ### Why use-stack?
49
+
50
+ - **Zero Dependencies** — Pure React implementation
51
+ - **TypeScript First** — Full `<T>` generics with exported types
52
+ - **Immutable Updates** — New array on every change; `readonly T[]` return type prevents accidental in-place mutation
53
+ - **True LIFO** — `push` and `pop` both operate on the top; `pop` **returns the popped item**
54
+ - **Peek** — Read the top item without mutating; stable and always current
55
+ - **Stable Actions** — Action identities never change, so they're safe as `useEffect` dependencies
56
+ - **No Wasted Renders** — No-op updates (empty `push`, `pop`/`clear` on an empty stack) are skipped
57
+ - **Lazy Initialization** — Accepts an array, an iterable, or a factory — just like `useState`
58
+
59
+ ---
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ # npm
65
+ npm install @usefy/use-stack
66
+
67
+ # yarn
68
+ yarn add @usefy/use-stack
69
+
70
+ # pnpm
71
+ pnpm add @usefy/use-stack
72
+ ```
73
+
74
+ ### Peer Dependencies
75
+
76
+ This package requires React 18 or 19:
77
+
78
+ ```json
79
+ {
80
+ "peerDependencies": {
81
+ "react": "^18.0.0 || ^19.0.0"
82
+ }
83
+ }
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Quick Start
89
+
90
+ ```tsx
91
+ import { useStack } from "@usefy/use-stack";
92
+
93
+ interface Snapshot {
94
+ id: number;
95
+ label: string;
96
+ }
97
+
98
+ function Editor() {
99
+ const [history, { push, pop, peek }] = useStack<Snapshot>([]);
100
+
101
+ const undo = () => {
102
+ const last = pop(); // remove + read the most recent change in one call
103
+ if (last) restore(last);
104
+ };
105
+
106
+ return (
107
+ <div>
108
+ <button onClick={() => push({ id: Date.now(), label: "Edit" })}>
109
+ Record change
110
+ </button>
111
+ <button onClick={undo} disabled={history.length === 0}>
112
+ Undo {peek()?.label}
113
+ </button>
114
+ <p>History depth: {history.length}</p>
115
+ </div>
116
+ );
117
+ }
118
+ ```
119
+
120
+ ---
121
+
122
+ ## API Reference
123
+
124
+ ### `useStack<T>(initialState?)`
125
+
126
+ Returns a tuple of the current read-only stack and a stable actions object.
127
+
128
+ #### Parameters
129
+
130
+ | Parameter | Type | Default | Description |
131
+ | -------------- | ---------------------- | ------- | ------------------------------------------------------------------------------ |
132
+ | `initialState` | `StackInitializer<T>` | empty | An array, an iterable of values, or a factory returning one (evaluated once). The last element becomes the top. |
133
+
134
+ #### Returns `[stack, actions]`
135
+
136
+ | Item | Type | Description |
137
+ | --------- | --------------------- | --------------------------------------------------------------------- |
138
+ | `stack` | `readonly T[]` | Current stack. The **last** item is the top; `stack[0]` is the bottom |
139
+ | `actions` | `UseStackActions<T>` | Stable action handlers (see below) |
140
+
141
+ #### Actions
142
+
143
+ | Action | Signature | Description |
144
+ | ------- | ---------------------------- | --------------------------------------------------------------------------------- |
145
+ | `push` | `(...items: T[]) => void` | Push one or more items onto the top (the last argument ends up on top). No items is a no-op |
146
+ | `pop` | `() => T \| undefined` | Remove the top item and return it. Returns `undefined` (no-op) when empty |
147
+ | `peek` | `() => T \| undefined` | Read the top item without mutating (stable, always reflects latest state) |
148
+ | `clear` | `() => void` | Remove all items. Clearing an empty stack is a no-op |
149
+ | `reset` | `() => void` | Restore the initial values (a fresh copy) |
150
+
151
+ #### Reading `top` / `bottom` / `size`
152
+
153
+ The returned stack is a read-only array, so these derive directly from it — no extra state needed:
154
+
155
+ ```tsx
156
+ const [stack] = useStack<number>([1, 2, 3]);
157
+
158
+ const top = stack[stack.length - 1]; // 3 (next to pop)
159
+ const bottom = stack[0]; // 1 (oldest item)
160
+ const size = stack.length; // 3
161
+ ```
162
+
163
+ > The returned `stack` is a `readonly T[]`, so calling `stack.push(...)` directly is a TypeScript error. Use the actions — mutating the array in place would bypass React state and break re-renders.
164
+
165
+ ---
166
+
167
+ ## Examples
168
+
169
+ ### Undo history
170
+
171
+ ```tsx
172
+ import { useStack } from "@usefy/use-stack";
173
+
174
+ function Editor() {
175
+ const [history, { push, pop }] = useStack<Snapshot>([]);
176
+
177
+ const record = (snapshot: Snapshot) => push(snapshot);
178
+
179
+ const undo = () => {
180
+ const last = pop();
181
+ if (last) restore(last);
182
+ };
183
+
184
+ return (
185
+ <div>
186
+ <button onClick={() => record(snapshot())}>Record</button>
187
+ <button onClick={undo} disabled={history.length === 0}>
188
+ Undo ({history.length})
189
+ </button>
190
+ </div>
191
+ );
192
+ }
193
+ ```
194
+
195
+ ### Push, pop from the top (LIFO)
196
+
197
+ ```tsx
198
+ const [stack, { push, pop, reset }] = useStack<number>([1, 2]);
199
+
200
+ push(3, 4); // stack: [1, 2, 3, 4] (4 is the top)
201
+ pop(); // returns 4, stack: [1, 2, 3]
202
+ pop(); // returns 3, stack: [1, 2]
203
+ reset(); // back to [1, 2]
204
+ ```
205
+
206
+ ### Peek before popping
207
+
208
+ ```tsx
209
+ const [stack, { peek, pop }] = useStack<Frame>([]);
210
+
211
+ // Decide based on the top without removing it
212
+ if (peek()?.type === "modal") {
213
+ const frame = pop();
214
+ close(frame!);
215
+ }
216
+ ```
217
+
218
+ ### Stable actions as effect dependencies
219
+
220
+ ```tsx
221
+ const [stack, actions] = useStack<Route>();
222
+
223
+ useEffect(() => {
224
+ const unsub = router.onNavigate((route) => actions.push(route));
225
+ return unsub;
226
+ }, [actions]); // actions never changes identity — effect runs once
227
+ ```
228
+
229
+ ---
230
+
231
+ ## TypeScript
232
+
233
+ ```tsx
234
+ import {
235
+ useStack,
236
+ type StackInitializer,
237
+ type UseStackActions,
238
+ type UseStackReturn,
239
+ } from "@usefy/use-stack";
240
+
241
+ const [stack, actions]: UseStackReturn<number> = useStack<number>([1, 2, 3]);
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Behavior Notes
247
+
248
+ - **LIFO** — `push` appends to the top; `pop` and `peek` operate on the same end (the last element, `stack[stack.length - 1]`).
249
+ - **`pop` returns the item** — It removes and returns the top element in one call, or `undefined` when the stack is empty.
250
+ - **Batched `pop` caveat** — The value `pop` returns is the top at call time. If you call `pop` multiple times synchronously (before React re-renders), each call returns the same top snapshot, though the settled state shrinks correctly by that many items. Read the returned `stack` array after the render for the final state.
251
+ - **Immutable** — Actions never mutate the current array; they replace it with a new one. Any snapshot you captured stays valid.
252
+ - **Referentially stable actions** — The actions object and each function keep the same identity for the lifetime of the component.
253
+ - **Initial value is copied** — The array/iterable you pass in is never mutated, and `reset` always yields a fresh copy of it.
254
+ - **No-op skipping** — Updates that wouldn't change anything don't create a new array or trigger a re-render.
255
+
256
+ ---
257
+
258
+ ## Testing
259
+
260
+ This package maintains comprehensive test coverage to ensure reliability and stability.
261
+
262
+ ### Test Coverage
263
+
264
+ 📊 <a href="https://mirunamu00.github.io/usefy/coverage/use-stack/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages)
265
+
266
+ ### Test Files
267
+
268
+ - `useStack.test.ts` — 25 tests for hook behavior, LIFO semantics, and immutability
269
+
270
+ **Total: 25 tests**
271
+
272
+ ---
273
+
274
+ ## License
275
+
276
+ MIT © [mirunamu](https://github.com/mirunamu00)
277
+
278
+ This package is part of the [usefy](https://github.com/mirunamu00/usefy) monorepo.
279
+
280
+ ---
281
+
282
+ <p align="center">
283
+ <sub>Built with care by the usefy team</sub>
284
+ </p>
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Accepted initial value for {@link useStack}.
3
+ *
4
+ * Mirrors `useState`'s lazy-initializer support: pass an array, any iterable of
5
+ * values, or a function returning one of those (evaluated once on mount). The
6
+ * **last** element becomes the top of the stack (the next item to be popped).
7
+ *
8
+ * @template T - Element type.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * useStack<string>(); // empty
13
+ * useStack<string>(["a", "b"]); // "b" is the top
14
+ * useStack(new Set(["a", "b"])); // any iterable works
15
+ * useStack(() => expensiveInit()); // lazy
16
+ * ```
17
+ */
18
+ type StackInitializer<T> = Iterable<T> | (() => Iterable<T>);
19
+ /**
20
+ * Stable action handlers returned by {@link useStack}. Every function keeps a
21
+ * stable identity across renders, so the actions object is safe to use as a
22
+ * `useEffect`/`useMemo` dependency.
23
+ *
24
+ * @template T - Element type.
25
+ */
26
+ interface UseStackActions<T> {
27
+ /**
28
+ * Push one or more items onto the top of the stack. Items are pushed in
29
+ * argument order, so the **last** argument ends up on top. Calling with no
30
+ * arguments is a no-op and does not trigger a re-render.
31
+ */
32
+ push: (...items: T[]) => void;
33
+ /**
34
+ * Pop the top item: remove it from the stack and return it. Returns
35
+ * `undefined` when the stack is empty (in which case it is a no-op and does
36
+ * not trigger a re-render).
37
+ *
38
+ * The returned value reflects the top of the stack at call time. When `pop`
39
+ * is called multiple times synchronously within the same event (before React
40
+ * re-renders), each call returns the same top snapshot even though the
41
+ * underlying state correctly shrinks — read the returned `stack` array after
42
+ * the render for the settled state.
43
+ */
44
+ pop: () => T | undefined;
45
+ /**
46
+ * Peek at the top item (the next item that `pop` would return) without
47
+ * mutating the stack. Returns `undefined` when the stack is empty. Stable
48
+ * across renders and always reflects the latest state.
49
+ */
50
+ peek: () => T | undefined;
51
+ /**
52
+ * Remove every item. Clearing an already-empty stack is a no-op.
53
+ */
54
+ clear: () => void;
55
+ /**
56
+ * Reset the stack back to the initial value provided at mount (a fresh copy,
57
+ * so later mutations never affect the stored initial).
58
+ */
59
+ reset: () => void;
60
+ }
61
+ /**
62
+ * Return type of {@link useStack}: a tuple of the current (read-only) stack and
63
+ * the stable action handlers.
64
+ *
65
+ * The stack is typed as `readonly T[]` on purpose — mutating it directly (e.g.
66
+ * `stack.push(...)`) would bypass React state and break immutability, so the
67
+ * type steers callers to the actions instead. Read access works as usual:
68
+ * `stack[stack.length - 1]` is the top, `stack[0]` is the bottom, and
69
+ * `stack.length` is the size.
70
+ *
71
+ * @template T - Element type.
72
+ */
73
+ type UseStackReturn<T> = [readonly T[], UseStackActions<T>];
74
+
75
+ /**
76
+ * A React hook for managing a LIFO (last-in, first-out) stack as React state
77
+ * with immutable, ergonomic updates.
78
+ *
79
+ * The LIFO sibling of {@link https://npmjs.com/package/@usefy/use-queue | useQueue}
80
+ * — identical in shape and conventions, but pushes and pops from the same end.
81
+ *
82
+ * Returns a tuple of the current (read-only) stack and a stable set of actions.
83
+ * The **top** of the stack is the last element (`stack[stack.length - 1]`, the
84
+ * next item to be popped) and new items are pushed onto the end. Every mutation
85
+ * produces a brand-new array so React re-renders correctly and the previous
86
+ * state is never mutated in place. Updates that would not change anything
87
+ * (pushing nothing, popping from or clearing an empty stack) are skipped to
88
+ * avoid needless re-renders.
89
+ *
90
+ * Features:
91
+ * - Immutable updates (new array on every change) with a `readonly T[]` return type
92
+ * - LIFO semantics: `push` and `pop` both operate on the top (the array's end)
93
+ * - `pop` returns the popped item (or `undefined` when empty)
94
+ * - `peek` reads the top item without mutating; stable and always current
95
+ * - Stable action identities — safe to use as effect dependencies
96
+ * - `useState`-style lazy initialization; accepts an array, iterable, or factory
97
+ * - Full TypeScript generics for the element type
98
+ *
99
+ * Reading `top` / `bottom` / `size` is done directly on the returned stack:
100
+ * `stack[stack.length - 1]`, `stack[0]`, and `stack.length` respectively.
101
+ *
102
+ * @template T - Element type.
103
+ * @param initialState - Initial items, or a factory returning them. Defaults to empty.
104
+ * @returns `[stack, { push, pop, peek, clear, reset }]`
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * // An undo / navigation history stack
109
+ * interface Snapshot { id: number; label: string }
110
+ *
111
+ * function Editor() {
112
+ * const [history, { push, pop, peek }] = useStack<Snapshot>([]);
113
+ *
114
+ * const undo = () => {
115
+ * const last = pop(); // remove + read the most recent change in one call
116
+ * if (last) restore(last);
117
+ * };
118
+ *
119
+ * return (
120
+ * <div>
121
+ * <button onClick={() => push({ id: Date.now(), label: "Edit" })}>
122
+ * Record change
123
+ * </button>
124
+ * <button onClick={undo} disabled={history.length === 0}>
125
+ * Undo{peek() ? ` (${peek()!.label})` : ""}
126
+ * </button>
127
+ * <p>History depth: {history.length}</p>
128
+ * </div>
129
+ * );
130
+ * }
131
+ * ```
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * // Push, pop from the top (LIFO), reset
136
+ * const [s, { push, pop, reset }] = useStack<number>([1, 2]);
137
+ * push(3, 4); // stack: [1, 2, 3, 4] (4 is the top)
138
+ * pop(); // returns 4, stack: [1, 2, 3]
139
+ * reset(); // back to [1, 2]
140
+ * ```
141
+ */
142
+ declare function useStack<T>(initialState?: StackInitializer<T>): UseStackReturn<T>;
143
+
144
+ export { type StackInitializer, type UseStackActions, type UseStackReturn, useStack };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Accepted initial value for {@link useStack}.
3
+ *
4
+ * Mirrors `useState`'s lazy-initializer support: pass an array, any iterable of
5
+ * values, or a function returning one of those (evaluated once on mount). The
6
+ * **last** element becomes the top of the stack (the next item to be popped).
7
+ *
8
+ * @template T - Element type.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * useStack<string>(); // empty
13
+ * useStack<string>(["a", "b"]); // "b" is the top
14
+ * useStack(new Set(["a", "b"])); // any iterable works
15
+ * useStack(() => expensiveInit()); // lazy
16
+ * ```
17
+ */
18
+ type StackInitializer<T> = Iterable<T> | (() => Iterable<T>);
19
+ /**
20
+ * Stable action handlers returned by {@link useStack}. Every function keeps a
21
+ * stable identity across renders, so the actions object is safe to use as a
22
+ * `useEffect`/`useMemo` dependency.
23
+ *
24
+ * @template T - Element type.
25
+ */
26
+ interface UseStackActions<T> {
27
+ /**
28
+ * Push one or more items onto the top of the stack. Items are pushed in
29
+ * argument order, so the **last** argument ends up on top. Calling with no
30
+ * arguments is a no-op and does not trigger a re-render.
31
+ */
32
+ push: (...items: T[]) => void;
33
+ /**
34
+ * Pop the top item: remove it from the stack and return it. Returns
35
+ * `undefined` when the stack is empty (in which case it is a no-op and does
36
+ * not trigger a re-render).
37
+ *
38
+ * The returned value reflects the top of the stack at call time. When `pop`
39
+ * is called multiple times synchronously within the same event (before React
40
+ * re-renders), each call returns the same top snapshot even though the
41
+ * underlying state correctly shrinks — read the returned `stack` array after
42
+ * the render for the settled state.
43
+ */
44
+ pop: () => T | undefined;
45
+ /**
46
+ * Peek at the top item (the next item that `pop` would return) without
47
+ * mutating the stack. Returns `undefined` when the stack is empty. Stable
48
+ * across renders and always reflects the latest state.
49
+ */
50
+ peek: () => T | undefined;
51
+ /**
52
+ * Remove every item. Clearing an already-empty stack is a no-op.
53
+ */
54
+ clear: () => void;
55
+ /**
56
+ * Reset the stack back to the initial value provided at mount (a fresh copy,
57
+ * so later mutations never affect the stored initial).
58
+ */
59
+ reset: () => void;
60
+ }
61
+ /**
62
+ * Return type of {@link useStack}: a tuple of the current (read-only) stack and
63
+ * the stable action handlers.
64
+ *
65
+ * The stack is typed as `readonly T[]` on purpose — mutating it directly (e.g.
66
+ * `stack.push(...)`) would bypass React state and break immutability, so the
67
+ * type steers callers to the actions instead. Read access works as usual:
68
+ * `stack[stack.length - 1]` is the top, `stack[0]` is the bottom, and
69
+ * `stack.length` is the size.
70
+ *
71
+ * @template T - Element type.
72
+ */
73
+ type UseStackReturn<T> = [readonly T[], UseStackActions<T>];
74
+
75
+ /**
76
+ * A React hook for managing a LIFO (last-in, first-out) stack as React state
77
+ * with immutable, ergonomic updates.
78
+ *
79
+ * The LIFO sibling of {@link https://npmjs.com/package/@usefy/use-queue | useQueue}
80
+ * — identical in shape and conventions, but pushes and pops from the same end.
81
+ *
82
+ * Returns a tuple of the current (read-only) stack and a stable set of actions.
83
+ * The **top** of the stack is the last element (`stack[stack.length - 1]`, the
84
+ * next item to be popped) and new items are pushed onto the end. Every mutation
85
+ * produces a brand-new array so React re-renders correctly and the previous
86
+ * state is never mutated in place. Updates that would not change anything
87
+ * (pushing nothing, popping from or clearing an empty stack) are skipped to
88
+ * avoid needless re-renders.
89
+ *
90
+ * Features:
91
+ * - Immutable updates (new array on every change) with a `readonly T[]` return type
92
+ * - LIFO semantics: `push` and `pop` both operate on the top (the array's end)
93
+ * - `pop` returns the popped item (or `undefined` when empty)
94
+ * - `peek` reads the top item without mutating; stable and always current
95
+ * - Stable action identities — safe to use as effect dependencies
96
+ * - `useState`-style lazy initialization; accepts an array, iterable, or factory
97
+ * - Full TypeScript generics for the element type
98
+ *
99
+ * Reading `top` / `bottom` / `size` is done directly on the returned stack:
100
+ * `stack[stack.length - 1]`, `stack[0]`, and `stack.length` respectively.
101
+ *
102
+ * @template T - Element type.
103
+ * @param initialState - Initial items, or a factory returning them. Defaults to empty.
104
+ * @returns `[stack, { push, pop, peek, clear, reset }]`
105
+ *
106
+ * @example
107
+ * ```tsx
108
+ * // An undo / navigation history stack
109
+ * interface Snapshot { id: number; label: string }
110
+ *
111
+ * function Editor() {
112
+ * const [history, { push, pop, peek }] = useStack<Snapshot>([]);
113
+ *
114
+ * const undo = () => {
115
+ * const last = pop(); // remove + read the most recent change in one call
116
+ * if (last) restore(last);
117
+ * };
118
+ *
119
+ * return (
120
+ * <div>
121
+ * <button onClick={() => push({ id: Date.now(), label: "Edit" })}>
122
+ * Record change
123
+ * </button>
124
+ * <button onClick={undo} disabled={history.length === 0}>
125
+ * Undo{peek() ? ` (${peek()!.label})` : ""}
126
+ * </button>
127
+ * <p>History depth: {history.length}</p>
128
+ * </div>
129
+ * );
130
+ * }
131
+ * ```
132
+ *
133
+ * @example
134
+ * ```tsx
135
+ * // Push, pop from the top (LIFO), reset
136
+ * const [s, { push, pop, reset }] = useStack<number>([1, 2]);
137
+ * push(3, 4); // stack: [1, 2, 3, 4] (4 is the top)
138
+ * pop(); // returns 4, stack: [1, 2, 3]
139
+ * reset(); // back to [1, 2]
140
+ * ```
141
+ */
142
+ declare function useStack<T>(initialState?: StackInitializer<T>): UseStackReturn<T>;
143
+
144
+ export { type StackInitializer, type UseStackActions, type UseStackReturn, useStack };
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
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 index_exports = {};
22
+ __export(index_exports, {
23
+ useStack: () => useStack
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/useStack.ts
28
+ var import_react = require("react");
29
+ function resolveInitial(initial) {
30
+ const value = typeof initial === "function" ? initial() : initial;
31
+ return value ? [...value] : [];
32
+ }
33
+ function useStack(initialState) {
34
+ const initialRef = (0, import_react.useRef)(null);
35
+ if (initialRef.current === null) {
36
+ initialRef.current = resolveInitial(initialState);
37
+ }
38
+ const [stack, setStack] = (0, import_react.useState)(() => [
39
+ ...initialRef.current
40
+ ]);
41
+ const stackStateRef = (0, import_react.useRef)(stack);
42
+ stackStateRef.current = stack;
43
+ const push = (0, import_react.useCallback)((...items) => {
44
+ setStack((prev) => items.length === 0 ? prev : [...prev, ...items]);
45
+ }, []);
46
+ const pop = (0, import_react.useCallback)(() => {
47
+ const top = stackStateRef.current[stackStateRef.current.length - 1];
48
+ setStack((prev) => prev.length === 0 ? prev : prev.slice(0, -1));
49
+ return top;
50
+ }, []);
51
+ const peek = (0, import_react.useCallback)(
52
+ () => stackStateRef.current[stackStateRef.current.length - 1],
53
+ []
54
+ );
55
+ const clear = (0, import_react.useCallback)(() => {
56
+ setStack((prev) => prev.length === 0 ? prev : []);
57
+ }, []);
58
+ const reset = (0, import_react.useCallback)(() => {
59
+ setStack([...initialRef.current]);
60
+ }, []);
61
+ const actions = (0, import_react.useMemo)(
62
+ () => ({ push, pop, peek, clear, reset }),
63
+ [push, pop, peek, clear, reset]
64
+ );
65
+ return [stack, actions];
66
+ }
67
+ // Annotate the CommonJS export names for ESM import in node:
68
+ 0 && (module.exports = {
69
+ useStack
70
+ });
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/useStack.ts"],"sourcesContent":["export { useStack } from \"./useStack\";\nexport type {\n StackInitializer,\n UseStackActions,\n UseStackReturn,\n} from \"./types\";\n","import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type {\n StackInitializer,\n UseStackActions,\n UseStackReturn,\n} from \"./types\";\n\n/**\n * Resolve a {@link StackInitializer} to a concrete array. A function\n * initializer is invoked once; any iterable is copied into a fresh array so the\n * caller's original object is never mutated.\n */\nfunction resolveInitial<T>(initial?: StackInitializer<T>): T[] {\n const value = typeof initial === \"function\" ? initial() : initial;\n return value ? [...value] : [];\n}\n\n/**\n * A React hook for managing a LIFO (last-in, first-out) stack as React state\n * with immutable, ergonomic updates.\n *\n * The LIFO sibling of {@link https://npmjs.com/package/@usefy/use-queue | useQueue}\n * — identical in shape and conventions, but pushes and pops from the same end.\n *\n * Returns a tuple of the current (read-only) stack and a stable set of actions.\n * The **top** of the stack is the last element (`stack[stack.length - 1]`, the\n * next item to be popped) and new items are pushed onto the end. Every mutation\n * produces a brand-new array so React re-renders correctly and the previous\n * state is never mutated in place. Updates that would not change anything\n * (pushing nothing, popping from or clearing an empty stack) are skipped to\n * avoid needless re-renders.\n *\n * Features:\n * - Immutable updates (new array on every change) with a `readonly T[]` return type\n * - LIFO semantics: `push` and `pop` both operate on the top (the array's end)\n * - `pop` returns the popped item (or `undefined` when empty)\n * - `peek` reads the top item without mutating; stable and always current\n * - Stable action identities — safe to use as effect dependencies\n * - `useState`-style lazy initialization; accepts an array, iterable, or factory\n * - Full TypeScript generics for the element type\n *\n * Reading `top` / `bottom` / `size` is done directly on the returned stack:\n * `stack[stack.length - 1]`, `stack[0]`, and `stack.length` respectively.\n *\n * @template T - Element type.\n * @param initialState - Initial items, or a factory returning them. Defaults to empty.\n * @returns `[stack, { push, pop, peek, clear, reset }]`\n *\n * @example\n * ```tsx\n * // An undo / navigation history stack\n * interface Snapshot { id: number; label: string }\n *\n * function Editor() {\n * const [history, { push, pop, peek }] = useStack<Snapshot>([]);\n *\n * const undo = () => {\n * const last = pop(); // remove + read the most recent change in one call\n * if (last) restore(last);\n * };\n *\n * return (\n * <div>\n * <button onClick={() => push({ id: Date.now(), label: \"Edit\" })}>\n * Record change\n * </button>\n * <button onClick={undo} disabled={history.length === 0}>\n * Undo{peek() ? ` (${peek()!.label})` : \"\"}\n * </button>\n * <p>History depth: {history.length}</p>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Push, pop from the top (LIFO), reset\n * const [s, { push, pop, reset }] = useStack<number>([1, 2]);\n * push(3, 4); // stack: [1, 2, 3, 4] (4 is the top)\n * pop(); // returns 4, stack: [1, 2, 3]\n * reset(); // back to [1, 2]\n * ```\n */\nexport function useStack<T>(\n initialState?: StackInitializer<T>\n): UseStackReturn<T> {\n // Resolve the initial stack exactly once and keep it for `reset`.\n const initialRef = useRef<T[] | null>(null);\n if (initialRef.current === null) {\n initialRef.current = resolveInitial(initialState);\n }\n\n const [stack, setStack] = useState<T[]>(() => [\n ...(initialRef.current as T[]),\n ]);\n\n // Mirror the latest stack so `pop`/`peek` can be stable callbacks that still\n // read fresh state.\n const stackStateRef = useRef(stack);\n stackStateRef.current = stack;\n\n const push = useCallback((...items: T[]) => {\n setStack((prev) => (items.length === 0 ? prev : [...prev, ...items]));\n }, []);\n\n const pop = useCallback((): T | undefined => {\n // Capture the top from the mirrored state so we can return it.\n const top = stackStateRef.current[stackStateRef.current.length - 1];\n setStack((prev) => (prev.length === 0 ? prev : prev.slice(0, -1)));\n return top;\n }, []);\n\n const peek = useCallback(\n (): T | undefined =>\n stackStateRef.current[stackStateRef.current.length - 1],\n []\n );\n\n const clear = useCallback(() => {\n setStack((prev) => (prev.length === 0 ? prev : []));\n }, []);\n\n const reset = useCallback(() => {\n setStack([...(initialRef.current as T[])]);\n }, []);\n\n const actions = useMemo<UseStackActions<T>>(\n () => ({ push, pop, peek, clear, reset }),\n [push, pop, peek, clear, reset]\n );\n\n return [stack, actions];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAuD;AAYvD,SAAS,eAAkB,SAAoC;AAC7D,QAAM,QAAQ,OAAO,YAAY,aAAa,QAAQ,IAAI;AAC1D,SAAO,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;AAC/B;AAqEO,SAAS,SACd,cACmB;AAEnB,QAAM,iBAAa,qBAAmB,IAAI;AAC1C,MAAI,WAAW,YAAY,MAAM;AAC/B,eAAW,UAAU,eAAe,YAAY;AAAA,EAClD;AAEA,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAc,MAAM;AAAA,IAC5C,GAAI,WAAW;AAAA,EACjB,CAAC;AAID,QAAM,oBAAgB,qBAAO,KAAK;AAClC,gBAAc,UAAU;AAExB,QAAM,WAAO,0BAAY,IAAI,UAAe;AAC1C,aAAS,CAAC,SAAU,MAAM,WAAW,IAAI,OAAO,CAAC,GAAG,MAAM,GAAG,KAAK,CAAE;AAAA,EACtE,GAAG,CAAC,CAAC;AAEL,QAAM,UAAM,0BAAY,MAAqB;AAE3C,UAAM,MAAM,cAAc,QAAQ,cAAc,QAAQ,SAAS,CAAC;AAClE,aAAS,CAAC,SAAU,KAAK,WAAW,IAAI,OAAO,KAAK,MAAM,GAAG,EAAE,CAAE;AACjE,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,QAAM,WAAO;AAAA,IACX,MACE,cAAc,QAAQ,cAAc,QAAQ,SAAS,CAAC;AAAA,IACxD,CAAC;AAAA,EACH;AAEA,QAAM,YAAQ,0BAAY,MAAM;AAC9B,aAAS,CAAC,SAAU,KAAK,WAAW,IAAI,OAAO,CAAC,CAAE;AAAA,EACpD,GAAG,CAAC,CAAC;AAEL,QAAM,YAAQ,0BAAY,MAAM;AAC9B,aAAS,CAAC,GAAI,WAAW,OAAe,CAAC;AAAA,EAC3C,GAAG,CAAC,CAAC;AAEL,QAAM,cAAU;AAAA,IACd,OAAO,EAAE,MAAM,KAAK,MAAM,OAAO,MAAM;AAAA,IACvC,CAAC,MAAM,KAAK,MAAM,OAAO,KAAK;AAAA,EAChC;AAEA,SAAO,CAAC,OAAO,OAAO;AACxB;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,44 @@
1
+ // src/useStack.ts
2
+ import { useCallback, useMemo, useRef, useState } from "react";
3
+ function resolveInitial(initial) {
4
+ const value = typeof initial === "function" ? initial() : initial;
5
+ return value ? [...value] : [];
6
+ }
7
+ function useStack(initialState) {
8
+ const initialRef = useRef(null);
9
+ if (initialRef.current === null) {
10
+ initialRef.current = resolveInitial(initialState);
11
+ }
12
+ const [stack, setStack] = useState(() => [
13
+ ...initialRef.current
14
+ ]);
15
+ const stackStateRef = useRef(stack);
16
+ stackStateRef.current = stack;
17
+ const push = useCallback((...items) => {
18
+ setStack((prev) => items.length === 0 ? prev : [...prev, ...items]);
19
+ }, []);
20
+ const pop = useCallback(() => {
21
+ const top = stackStateRef.current[stackStateRef.current.length - 1];
22
+ setStack((prev) => prev.length === 0 ? prev : prev.slice(0, -1));
23
+ return top;
24
+ }, []);
25
+ const peek = useCallback(
26
+ () => stackStateRef.current[stackStateRef.current.length - 1],
27
+ []
28
+ );
29
+ const clear = useCallback(() => {
30
+ setStack((prev) => prev.length === 0 ? prev : []);
31
+ }, []);
32
+ const reset = useCallback(() => {
33
+ setStack([...initialRef.current]);
34
+ }, []);
35
+ const actions = useMemo(
36
+ () => ({ push, pop, peek, clear, reset }),
37
+ [push, pop, peek, clear, reset]
38
+ );
39
+ return [stack, actions];
40
+ }
41
+ export {
42
+ useStack
43
+ };
44
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useStack.ts"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type {\n StackInitializer,\n UseStackActions,\n UseStackReturn,\n} from \"./types\";\n\n/**\n * Resolve a {@link StackInitializer} to a concrete array. A function\n * initializer is invoked once; any iterable is copied into a fresh array so the\n * caller's original object is never mutated.\n */\nfunction resolveInitial<T>(initial?: StackInitializer<T>): T[] {\n const value = typeof initial === \"function\" ? initial() : initial;\n return value ? [...value] : [];\n}\n\n/**\n * A React hook for managing a LIFO (last-in, first-out) stack as React state\n * with immutable, ergonomic updates.\n *\n * The LIFO sibling of {@link https://npmjs.com/package/@usefy/use-queue | useQueue}\n * — identical in shape and conventions, but pushes and pops from the same end.\n *\n * Returns a tuple of the current (read-only) stack and a stable set of actions.\n * The **top** of the stack is the last element (`stack[stack.length - 1]`, the\n * next item to be popped) and new items are pushed onto the end. Every mutation\n * produces a brand-new array so React re-renders correctly and the previous\n * state is never mutated in place. Updates that would not change anything\n * (pushing nothing, popping from or clearing an empty stack) are skipped to\n * avoid needless re-renders.\n *\n * Features:\n * - Immutable updates (new array on every change) with a `readonly T[]` return type\n * - LIFO semantics: `push` and `pop` both operate on the top (the array's end)\n * - `pop` returns the popped item (or `undefined` when empty)\n * - `peek` reads the top item without mutating; stable and always current\n * - Stable action identities — safe to use as effect dependencies\n * - `useState`-style lazy initialization; accepts an array, iterable, or factory\n * - Full TypeScript generics for the element type\n *\n * Reading `top` / `bottom` / `size` is done directly on the returned stack:\n * `stack[stack.length - 1]`, `stack[0]`, and `stack.length` respectively.\n *\n * @template T - Element type.\n * @param initialState - Initial items, or a factory returning them. Defaults to empty.\n * @returns `[stack, { push, pop, peek, clear, reset }]`\n *\n * @example\n * ```tsx\n * // An undo / navigation history stack\n * interface Snapshot { id: number; label: string }\n *\n * function Editor() {\n * const [history, { push, pop, peek }] = useStack<Snapshot>([]);\n *\n * const undo = () => {\n * const last = pop(); // remove + read the most recent change in one call\n * if (last) restore(last);\n * };\n *\n * return (\n * <div>\n * <button onClick={() => push({ id: Date.now(), label: \"Edit\" })}>\n * Record change\n * </button>\n * <button onClick={undo} disabled={history.length === 0}>\n * Undo{peek() ? ` (${peek()!.label})` : \"\"}\n * </button>\n * <p>History depth: {history.length}</p>\n * </div>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Push, pop from the top (LIFO), reset\n * const [s, { push, pop, reset }] = useStack<number>([1, 2]);\n * push(3, 4); // stack: [1, 2, 3, 4] (4 is the top)\n * pop(); // returns 4, stack: [1, 2, 3]\n * reset(); // back to [1, 2]\n * ```\n */\nexport function useStack<T>(\n initialState?: StackInitializer<T>\n): UseStackReturn<T> {\n // Resolve the initial stack exactly once and keep it for `reset`.\n const initialRef = useRef<T[] | null>(null);\n if (initialRef.current === null) {\n initialRef.current = resolveInitial(initialState);\n }\n\n const [stack, setStack] = useState<T[]>(() => [\n ...(initialRef.current as T[]),\n ]);\n\n // Mirror the latest stack so `pop`/`peek` can be stable callbacks that still\n // read fresh state.\n const stackStateRef = useRef(stack);\n stackStateRef.current = stack;\n\n const push = useCallback((...items: T[]) => {\n setStack((prev) => (items.length === 0 ? prev : [...prev, ...items]));\n }, []);\n\n const pop = useCallback((): T | undefined => {\n // Capture the top from the mirrored state so we can return it.\n const top = stackStateRef.current[stackStateRef.current.length - 1];\n setStack((prev) => (prev.length === 0 ? prev : prev.slice(0, -1)));\n return top;\n }, []);\n\n const peek = useCallback(\n (): T | undefined =>\n stackStateRef.current[stackStateRef.current.length - 1],\n []\n );\n\n const clear = useCallback(() => {\n setStack((prev) => (prev.length === 0 ? prev : []));\n }, []);\n\n const reset = useCallback(() => {\n setStack([...(initialRef.current as T[])]);\n }, []);\n\n const actions = useMemo<UseStackActions<T>>(\n () => ({ push, pop, peek, clear, reset }),\n [push, pop, peek, clear, reset]\n );\n\n return [stack, actions];\n}\n"],"mappings":";AAAA,SAAS,aAAa,SAAS,QAAQ,gBAAgB;AAYvD,SAAS,eAAkB,SAAoC;AAC7D,QAAM,QAAQ,OAAO,YAAY,aAAa,QAAQ,IAAI;AAC1D,SAAO,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;AAC/B;AAqEO,SAAS,SACd,cACmB;AAEnB,QAAM,aAAa,OAAmB,IAAI;AAC1C,MAAI,WAAW,YAAY,MAAM;AAC/B,eAAW,UAAU,eAAe,YAAY;AAAA,EAClD;AAEA,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAc,MAAM;AAAA,IAC5C,GAAI,WAAW;AAAA,EACjB,CAAC;AAID,QAAM,gBAAgB,OAAO,KAAK;AAClC,gBAAc,UAAU;AAExB,QAAM,OAAO,YAAY,IAAI,UAAe;AAC1C,aAAS,CAAC,SAAU,MAAM,WAAW,IAAI,OAAO,CAAC,GAAG,MAAM,GAAG,KAAK,CAAE;AAAA,EACtE,GAAG,CAAC,CAAC;AAEL,QAAM,MAAM,YAAY,MAAqB;AAE3C,UAAM,MAAM,cAAc,QAAQ,cAAc,QAAQ,SAAS,CAAC;AAClE,aAAS,CAAC,SAAU,KAAK,WAAW,IAAI,OAAO,KAAK,MAAM,GAAG,EAAE,CAAE;AACjE,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,QAAM,OAAO;AAAA,IACX,MACE,cAAc,QAAQ,cAAc,QAAQ,SAAS,CAAC;AAAA,IACxD,CAAC;AAAA,EACH;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,aAAS,CAAC,SAAU,KAAK,WAAW,IAAI,OAAO,CAAC,CAAE;AAAA,EACpD,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQ,YAAY,MAAM;AAC9B,aAAS,CAAC,GAAI,WAAW,OAAe,CAAC;AAAA,EAC3C,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU;AAAA,IACd,OAAO,EAAE,MAAM,KAAK,MAAM,OAAO,MAAM;AAAA,IACvC,CAAC,MAAM,KAAK,MAAM,OAAO,KAAK;AAAA,EAChC;AAEA,SAAO,CAAC,OAAO,OAAO;AACxB;","names":[]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@usefy/use-stack",
3
+ "version": "0.19.0",
4
+ "description": "A React hook for managing LIFO stack state with immutable updates",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "peerDependencies": {
20
+ "react": "^18.0.0 || ^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@testing-library/jest-dom": "^6.9.1",
24
+ "@testing-library/react": "^16.3.1",
25
+ "@testing-library/user-event": "^14.6.1",
26
+ "@types/react": "^19.0.0",
27
+ "jsdom": "^27.3.0",
28
+ "react": "^19.0.0",
29
+ "rimraf": "^6.0.1",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0",
32
+ "vitest": "^4.0.16"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/mirunamu00/usefy.git",
40
+ "directory": "packages/hooks/use-stack"
41
+ },
42
+ "license": "MIT",
43
+ "keywords": [
44
+ "react",
45
+ "hooks",
46
+ "stack",
47
+ "lifo",
48
+ "state",
49
+ "collection",
50
+ "data-structure",
51
+ "useStack"
52
+ ],
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "dev": "tsup --watch",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "typecheck": "tsc --noEmit",
59
+ "clean": "rimraf dist"
60
+ }
61
+ }