@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 +284 -0
- package/dist/index.d.mts +144 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +44 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +61 -0
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>
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|