stunk 1.2.4 → 1.4.5

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 CHANGED
@@ -73,30 +73,6 @@ count.set(10);
73
73
  // "Double count: 20"
74
74
  ```
75
75
 
76
- ## Batch Updates
77
-
78
- Batch Update group multiple **state changes** together and notify **subscribers** only once at the end of the **batch**. This is particularly useful for **optimizing performance** when you need to **update multiple** chunks at the same time.
79
-
80
- ```typescript
81
- import { chunk, batch } from "stunk";
82
-
83
- const nameChunk = chunk("Olamide");
84
- const ageChunk = chunk(30);
85
-
86
- batch(() => {
87
- nameChunk.set("AbdulAzeez");
88
- ageChunk.set(31);
89
- }); // Only one notification will be sent to subscribers
90
-
91
- // Nested batches are also supported
92
- batch(() => {
93
- firstName.set("Olanrewaju");
94
- batch(() => {
95
- age.set(29);
96
- });
97
- }); // Only one notification will be sent to subscribers
98
- ```
99
-
100
76
  ## State Selection
101
77
 
102
78
  Efficiently access and react to specific state parts:
@@ -120,59 +96,30 @@ nameChunk.subscribe((name) => console.log("Name changed:", name));
120
96
  nameChunk.set("Olamide"); // ❌ this will throw an error, because it is a readonly.
121
97
  ```
122
98
 
123
- ## Middleware
124
-
125
- Middleware allows you to customize how values are set in a **chunk**. For example, you can add **logging**, **validation**, or any custom behavior when a chunk's value changes.
126
-
127
- ```typescript
128
- import { chunk } from "stunk";
129
- import { logger, nonNegativeValidator } from "stunk/middleware";
130
-
131
- // You can also create yours and pass it chunk as the second param
132
-
133
- // Use middleware for logging and validation
134
- const age = chunk(25, [logger, nonNegativeValidator]);
135
-
136
- age.set(30); // Logs: "Setting value: 30"
137
- age.set(-5); // ❌ Throws an error: "Value must be non-negative!"
138
- ```
139
-
140
- ## Time Travel (Middleware)
99
+ ## Batch Updates
141
100
 
142
- The withHistory middleware extends a chunk to support undo and redo functionality. This allows you to navigate back and forth between previous states, making it useful for implementing features like undo/redo, form history, and state time travel.
101
+ Batch Update group multiple **state changes** together and notify **subscribers** only once at the end of the `batch`. This is particularly useful for **optimizing performance** when you need to **update multiple** chunks at the same time.
143
102
 
144
103
  ```typescript
145
- import { chunk } from "stunk";
146
- import { withHistory } from "stunk/midddleware";
147
-
148
- const counterChunk = withHistory(chunk(0));
149
-
150
- counterChunk.set(1);
151
- counterChunk.set(2);
152
-
153
- counterChunk.undo(); // Goes back to 1
154
- counterChunk.undo(); // Goes back to 0
155
-
156
- counterChunk.redo(); // Goes forward to 1
157
-
158
- counterChunk.canUndo(); // Returns `true` if there is a previous state to revert to..
159
- counterChunk.canRedo(); // Returns `true` if there is a next state to move to.
160
-
161
- counterChunk.getHistory(); // Returns an array of all the values in the history.
104
+ import { chunk, batch } from "stunk";
162
105
 
163
- counterChunk.clearHistory(); // Clears the history, keeping only the current value.
164
- ```
106
+ const nameChunk = chunk("Olamide");
107
+ const ageChunk = chunk(30);
165
108
 
166
- **Example: Limiting History Size (Optional)**
167
- You can specify a max history size to prevent excessive memory usage.
109
+ batch(() => {
110
+ nameChunk.set("AbdulAzeez");
111
+ ageChunk.set(31);
112
+ }); // Only one notification will be sent to subscribers
168
113
 
169
- ```ts
170
- const counter = withHistory(chunk(0), { maxHistory: 5 });
171
- // Only keeps the last 5 changes -- default is 100.
114
+ // Nested batches are also supported
115
+ batch(() => {
116
+ firstName.set("Olanrewaju");
117
+ batch(() => {
118
+ age.set(29);
119
+ });
120
+ }); // Only one notification will be sent to subscribers
172
121
  ```
173
122
 
174
- This prevents the history from growing indefinitely and ensures efficient memory usage.
175
-
176
123
  ## Computed
177
124
 
178
125
  Computed Chunks in Stunk allow you to create state derived from other chunks in a reactive way. Unlike derived chunks, computed chunks can depend on multiple sources, and they automatically recalculate when any of the source chunks change.
@@ -202,10 +149,10 @@ firstNameChunk.set("Ola");
202
149
  ageChunk.set(10);
203
150
 
204
151
  console.log(fullInfoChunk.get());
205
- // ✅ { fullName: "Jane Doe", isAdult: true }
152
+ // ✅ { fullName: "Ola Doe", isAdult: true }
206
153
  ```
207
154
 
208
- Computed chunks are ideal for scenarios where state depends on multiple sources or needs complex calculations. They ensure your application remains performant and maintainable.
155
+ `computed` chunks are ideal for scenarios where state depends on multiple sources or needs complex calculations. They ensure your application remains performant and maintainable.
209
156
 
210
157
  ## Advanced Examples
211
158
 
@@ -277,7 +224,60 @@ const filteredPostsChunk = computed(
277
224
  );
278
225
  ```
279
226
 
280
- ## State Persistence
227
+ ## Middleware
228
+
229
+ Middleware allows you to customize how values are set in a **chunk**. For example, you can add **logging**, **validation**, or any custom behavior when a chunk's value changes.
230
+
231
+ ```typescript
232
+ import { chunk } from "stunk";
233
+ import { logger, nonNegativeValidator } from "stunk/middleware";
234
+
235
+ // You can also create yours and pass it chunk as the second param
236
+
237
+ // Use middleware for logging and validation
238
+ const age = chunk(25, [logger, nonNegativeValidator]);
239
+
240
+ age.set(30); // Logs: "Setting value: 30"
241
+ age.set(-5); // ❌ Throws an error: "Value must be non-negative!"
242
+ ```
243
+
244
+ ## Time Travel (Middleware)
245
+
246
+ The `withHistory` middleware extends a chunk to support undo and redo functionality. This allows you to navigate back and forth between previous states, making it useful for implementing features like undo/redo, form history, and state time travel.
247
+
248
+ ```typescript
249
+ import { chunk } from "stunk";
250
+ import { withHistory } from "stunk/midddleware";
251
+
252
+ const counterChunk = withHistory(chunk(0));
253
+
254
+ counterChunk.set(1);
255
+ counterChunk.set(2);
256
+
257
+ counterChunk.undo(); // Goes back to 1
258
+ counterChunk.undo(); // Goes back to 0
259
+
260
+ counterChunk.redo(); // Goes forward to 1
261
+
262
+ counterChunk.canUndo(); // Returns `true` if there is a previous state to revert to..
263
+ counterChunk.canRedo(); // Returns `true` if there is a next state to move to.
264
+
265
+ counterChunk.getHistory(); // Returns an array of all the values in the history.
266
+
267
+ counterChunk.clearHistory(); // Clears the history, keeping only the current value.
268
+ ```
269
+
270
+ **Example: Limiting History Size (Optional)**
271
+ You can specify a max history size to prevent excessive memory usage.
272
+
273
+ ```ts
274
+ const counter = withHistory(chunk(0), { maxHistory: 5 });
275
+ // Only keeps the last 5 changes -- default is 100.
276
+ ```
277
+
278
+ This prevents the history from growing indefinitely and ensures efficient memory usage.
279
+
280
+ ## State Persistence (Middleware)
281
281
 
282
282
  Stunk provides a persistence middleware to automatically save state changes to storage (localStorage, sessionStorage, etc).
283
283
 
@@ -314,6 +314,35 @@ const encryptedChunk = withPersistence(baseChunk, {
314
314
  });
315
315
  ```
316
316
 
317
+ ## Once
318
+
319
+ `Once` utility is a function that ensures a given piece of code or a function is executed only once, no matter how many times it's called. It's typically used to optimize performance by preventing redundant calculations or event handlers from running multiple times.
320
+
321
+ How It Works:
322
+
323
+ - It wraps a function and tracks whether it has been called.
324
+ - On the first call, it executes the function and saves the result.
325
+ - On subsequent calls, it simply returns the saved result without executing the function again.
326
+
327
+ ```typescript
328
+ const numbersChunk = chunk([1, 2, 3, 4, 5]);
329
+
330
+ const expensiveCalculation = once(() => {
331
+ console.log("Expensive calculation running...");
332
+ return numbersChunk.get().reduce((sum, num) => sum + num, 0);
333
+ });
334
+
335
+ // Derived chunk using the once utility
336
+ const totalChunk = numbersChunk.derive(() => expensiveCalculation());
337
+
338
+ totalChunk.subscribe((total) => {
339
+ console.log("Total:", total);
340
+ });
341
+
342
+ // Even if numbersChunk updates, the expensive calculation runs only once
343
+ numbersChunk.set([10, 20, 30, 40, 50]);
344
+ ```
345
+
317
346
  ## Async State
318
347
 
319
348
  Async Chunks in Stunk are designed to manage asynchronous state seamlessly. They handle loading, error, and data states automatically, making it easier to work with APIs and other asynchronous operations.
@@ -384,6 +413,53 @@ user.mutate(currentUser => ({
384
413
  }));
385
414
  ```
386
415
 
416
+ ## Combine Async Chunk
417
+
418
+ `combineAsyncChunks` utility is used for managing multiple related async chunks.
419
+
420
+ - Maintains reactivity through the entire chain
421
+ - Preserves previous data during reloading
422
+ - Proper error propagation
423
+
424
+ ```typescript
425
+ // Basic fetch
426
+ const userChunk = asyncChunk(async () => {
427
+ const response = await fetch("/api/user");
428
+ return response.json();
429
+ });
430
+
431
+ // With options
432
+ const postsChunk = asyncChunk(
433
+ async () => {
434
+ const response = await fetch("/api/posts");
435
+ return response.json();
436
+ },
437
+ {
438
+ initialData: [],
439
+ retryCount: 3,
440
+ retryDelay: 2000,
441
+ onError: (error) => console.error("Failed to fetch posts:", error),
442
+ }
443
+ );
444
+
445
+ // Combining chunks
446
+ const profileChunk = combineAsyncChunks({
447
+ user: userChunk,
448
+ posts: postsChunk,
449
+ });
450
+
451
+ // Reactive updates
452
+ profileChunk.subscribe(({ loading, error, data }) => {
453
+ if (loading) {
454
+ showLoadingSpinner();
455
+ } else if (error) {
456
+ showError(error);
457
+ } else {
458
+ updateUI(data);
459
+ }
460
+ });
461
+ ```
462
+
387
463
  ## API Reference
388
464
 
389
465
  ### Core
package/dist/index.js CHANGED
@@ -2,5 +2,5 @@ export { chunk, batch } from './core/core';
2
2
  export { asyncChunk } from './core/asyncChunk';
3
3
  export { computed } from './core/computed';
4
4
  export { select } from './core/selector';
5
- export { combineAsyncChunks } from './utils';
6
- export * from "./middleware";
5
+ export { combineAsyncChunks, once, isChunk, isValidChunkValue } from './utils';
6
+ export * as middleware from "./middleware";
@@ -0,0 +1,27 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ /**
3
+ * A hook that handles asynchronous state with built-in reactivity.
4
+ * Provides loading, error, and data states.
5
+ */
6
+ export function useAsyncChunk(asyncChunk) {
7
+ const [state, setState] = useState(() => asyncChunk.get());
8
+ useEffect(() => {
9
+ const unsubscribe = asyncChunk.subscribe((newState) => {
10
+ setState(newState);
11
+ });
12
+ return () => unsubscribe();
13
+ }, [asyncChunk]);
14
+ const reload = useCallback(() => asyncChunk.reload(), [asyncChunk]);
15
+ const mutate = useCallback((mutator) => asyncChunk.mutate(mutator), [asyncChunk]);
16
+ const reset = useCallback(() => asyncChunk.reset(), [asyncChunk]);
17
+ const { data, loading, error } = state;
18
+ return {
19
+ state,
20
+ data,
21
+ loading,
22
+ error,
23
+ reload,
24
+ mutate,
25
+ reset
26
+ };
27
+ }
@@ -0,0 +1,29 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { select } from "../../core/selector";
3
+ /**
4
+ * A lightweight hook that subscribes to a chunk and returns its current value, along with setters, selector, reset and destroy.
5
+ * Ensures reactivity and prevents unnecessary re-renders.
6
+ */
7
+ export function useChunk(chunk, selector) {
8
+ const selectedChunk = selector ? select(chunk, selector) : chunk;
9
+ const [state, setState] = useState(() => selectedChunk.get());
10
+ useEffect(() => {
11
+ const unsubscribe = selectedChunk.subscribe((newValue) => {
12
+ setState(() => newValue);
13
+ });
14
+ return () => unsubscribe();
15
+ }, [selectedChunk]);
16
+ const set = useCallback((value) => {
17
+ chunk.set(value);
18
+ }, [chunk]);
19
+ const update = useCallback((updater) => {
20
+ chunk.update(updater);
21
+ }, [chunk]);
22
+ const reset = useCallback(() => {
23
+ chunk.reset();
24
+ }, [chunk]);
25
+ const destroy = useCallback(() => {
26
+ chunk.destroy();
27
+ }, [chunk]);
28
+ return [state, set, update, reset, destroy];
29
+ }
@@ -0,0 +1,10 @@
1
+ import { useMemo } from "react";
2
+ import { useChunkValue } from "./useChunkValue";
3
+ /**
4
+ * A hook that subscribes to a specific property of a chunk.
5
+ * This optimizes renders by only updating when the selected property changes.
6
+ */
7
+ export function useChunkProperty(chunk, property) {
8
+ const selector = useMemo(() => (state) => state[property], [property]);
9
+ return useChunkValue(chunk, selector);
10
+ }
@@ -0,0 +1,9 @@
1
+ import { useChunk } from "./useChunk";
2
+ /**
3
+ * A lightweight hook that subscribes to a chunk and returns only its current value.
4
+ * Useful for read-only components that don't need to update the chunk.
5
+ */
6
+ export function useChunkValue(chunk, selector) {
7
+ const [value] = useChunk(chunk, selector);
8
+ return value;
9
+ }
@@ -0,0 +1,25 @@
1
+ import { useState, useEffect } from "react";
2
+ /**
3
+ * Hook to read values from multiple chunks at once.
4
+ * Only re-renders when any of the chunk values change.
5
+ */
6
+ export function useChunkValues(chunks) {
7
+ const [values, setValues] = useState(() => {
8
+ return chunks.map(chunk => chunk.get());
9
+ });
10
+ useEffect(() => {
11
+ const unsubscribes = chunks.map((chunk, index) => {
12
+ return chunk.subscribe((newValue) => {
13
+ setValues(prev => {
14
+ const newValues = [...prev];
15
+ newValues[index] = newValue;
16
+ return newValues;
17
+ });
18
+ });
19
+ });
20
+ return () => {
21
+ unsubscribes.forEach(unsubscribe => unsubscribe());
22
+ };
23
+ }, [chunks]);
24
+ return values;
25
+ }
@@ -0,0 +1,22 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+ import { computed } from "../../core/computed";
3
+ /**
4
+ * A hook that computes a value based on multiple chunks.
5
+ * Automatically re-computes when any dependency changes.
6
+ */
7
+ export function useComputed(dependencies, computeFn) {
8
+ // Create the computed value - memoize it based on dependencies to prevent recreation
9
+ const computedValue = useMemo(() => computed(dependencies, computeFn),
10
+ // eslint-disable-next-line react-hooks/exhaustive-deps
11
+ [...dependencies]);
12
+ const [state, setState] = useState(() => computedValue.get());
13
+ useEffect(() => {
14
+ const unsubscribe = computedValue.subscribe((newValue) => {
15
+ setState(newValue);
16
+ });
17
+ return () => {
18
+ unsubscribe();
19
+ };
20
+ }, [computedValue]);
21
+ return state;
22
+ }
@@ -0,0 +1,11 @@
1
+ import { useMemo } from "react";
2
+ import { useChunk } from "./useChunk";
3
+ /**
4
+ * A hook for creating a read-only derived value from a chunk.
5
+ * Ensures reactivity and updates when the source chunk changes.
6
+ */
7
+ export function useDerive(chunk, fn) {
8
+ const derivedChunk = useMemo(() => chunk.derive(fn), [chunk, fn]);
9
+ const [derivedValue] = useChunk(derivedChunk);
10
+ return derivedValue;
11
+ }
@@ -0,0 +1,7 @@
1
+ export { useChunk } from './hooks/useChunk';
2
+ export { useDerive } from './hooks/useDerive';
3
+ export { useComputed } from './hooks/useComputed';
4
+ export { useChunkValue } from './hooks/useChunkValue';
5
+ export { useChunkProperty } from './hooks/useChunkProperty';
6
+ export { useChunkValues } from './hooks/useChunkValues';
7
+ export { useAsyncChunk } from './hooks/useAsyncChunk';
package/dist/utils.js CHANGED
@@ -12,6 +12,18 @@ export function isChunk(value) {
12
12
  typeof value.reset === 'function' &&
13
13
  typeof value.destroy === 'function';
14
14
  }
15
+ export function once(fn) {
16
+ let called = false;
17
+ let result;
18
+ return () => {
19
+ if (!called) {
20
+ result = fn();
21
+ called = true;
22
+ }
23
+ return result;
24
+ };
25
+ }
26
+ ;
15
27
  export function combineAsyncChunks(chunks) {
16
28
  // Create initial state with proper typing
17
29
  const initialData = Object.keys(chunks).reduce((acc, key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stunk",
3
- "version": "1.2.4",
3
+ "version": "1.4.5",
4
4
  "description": "Stunk is a lightweight, framework-agnostic state management library for JavaScript and TypeScript. It uses chunk-based state units for efficient updates, reactivity, and performance optimization in React, Vue, Svelte, and Vanilla JS/TS applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,6 +12,20 @@
12
12
  },
13
13
  "main": "dist/index.js",
14
14
  "types": "dist/types/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./dist/index.js",
18
+ "types": "./dist/types/index.d.ts"
19
+ },
20
+ "./middleware": {
21
+ "import": "./dist/middleware/index.js",
22
+ "types": "./dist/types/middleware/index.d.ts"
23
+ },
24
+ "./react": {
25
+ "import": "./dist/use-react/index.js",
26
+ "types": "./dist/types/use-react/index.d.ts"
27
+ }
28
+ },
15
29
  "scripts": {
16
30
  "build": "tsc",
17
31
  "test": "jest --runInBand",
@@ -51,9 +65,15 @@
51
65
  "license": "MIT",
52
66
  "devDependencies": {
53
67
  "@types/jest": "^29.5.14",
68
+ "@types/react": "^19.0.10",
54
69
  "jest": "^29.7.0",
55
70
  "jest-environment-jsdom": "^29.7.0",
71
+ "react": "^19.0.0",
72
+ "react-dom": "^19.0.0",
56
73
  "ts-jest": "^29.2.5",
57
74
  "typescript": "^5.0.0"
75
+ },
76
+ "peerDependencies": {
77
+ "react": "^19.0.0"
58
78
  }
59
79
  }
@@ -1,10 +1,10 @@
1
- import { Chunk, chunk, batch } from "./core";
1
+ import { Chunk, chunk } from "./core";
2
2
 
3
3
  // Helper type to extract the value type from a Chunk
4
- type ChunkValue<T> = T extends Chunk<infer U> ? U : never;
4
+ export type ChunkValue<T> = T extends Chunk<infer U> ? U : never;
5
5
 
6
6
  // Helper type to transform an array of Chunks into an array of their value types
7
- type DependencyValues<T extends Chunk<any>[]> = {
7
+ export type DependencyValues<T extends Chunk<any>[]> = {
8
8
  [K in keyof T]: T[K] extends Chunk<any> ? ChunkValue<T[K]> : never;
9
9
  };
10
10
 
package/src/index.ts CHANGED
@@ -3,8 +3,8 @@ export { asyncChunk } from './core/asyncChunk';
3
3
  export { computed } from './core/computed';
4
4
  export { select } from './core/selector'
5
5
 
6
- export { combineAsyncChunks } from './utils';
6
+ export { combineAsyncChunks, once, isChunk, isValidChunkValue } from './utils';
7
7
 
8
8
  export type { Chunk, Middleware } from './core/core';
9
9
 
10
- export * from "./middleware";
10
+ export * as middleware from "./middleware";
@@ -0,0 +1,38 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+
3
+ import { AsyncChunk, AsyncState } from "../../core/asyncChunk";
4
+
5
+ /**
6
+ * A hook that handles asynchronous state with built-in reactivity.
7
+ * Provides loading, error, and data states.
8
+ */
9
+ export function useAsyncChunk<T>(asyncChunk: AsyncChunk<T>) {
10
+ const [state, setState] = useState<AsyncState<T>>(() => asyncChunk.get());
11
+
12
+ useEffect(() => {
13
+ const unsubscribe = asyncChunk.subscribe((newState) => {
14
+ setState(newState);
15
+ });
16
+
17
+ return () => unsubscribe();
18
+ }, [asyncChunk]);
19
+
20
+ const reload = useCallback(() => asyncChunk.reload(), [asyncChunk]);
21
+ const mutate = useCallback(
22
+ (mutator: (currentData: T | null) => T) => asyncChunk.mutate(mutator),
23
+ [asyncChunk]
24
+ );
25
+ const reset = useCallback(() => asyncChunk.reset(), [asyncChunk]);
26
+
27
+ const { data, loading, error } = state;
28
+
29
+ return {
30
+ state,
31
+ data,
32
+ loading,
33
+ error,
34
+ reload,
35
+ mutate,
36
+ reset
37
+ };
38
+ }
@@ -0,0 +1,47 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+
3
+ import { select } from "../../core/selector";
4
+
5
+ import type { Chunk } from "../../core/core";
6
+
7
+ /**
8
+ * A lightweight hook that subscribes to a chunk and returns its current value, along with setters, selector, reset and destroy.
9
+ * Ensures reactivity and prevents unnecessary re-renders.
10
+ */
11
+
12
+ export function useChunk<T, S = T>(
13
+ chunk: Chunk<T>,
14
+ selector?: (value: T) => S
15
+ ) {
16
+ const selectedChunk = selector ? select(chunk, selector) : chunk;
17
+
18
+ const [state, setState] = useState<S>(() => selectedChunk.get() as S);
19
+
20
+ useEffect(() => {
21
+ const unsubscribe = selectedChunk.subscribe((newValue) => {
22
+ setState(() => newValue as S);
23
+ });
24
+ return () => unsubscribe();
25
+ }, [selectedChunk]);
26
+
27
+ const set = useCallback((value: T) => {
28
+ chunk.set(value);
29
+ }, [chunk]);
30
+
31
+ const update = useCallback(
32
+ (updater: (currentValue: T) => T) => {
33
+ chunk.update(updater);
34
+ },
35
+ [chunk]
36
+ );
37
+
38
+ const reset = useCallback(() => {
39
+ chunk.reset();
40
+ }, [chunk]);
41
+
42
+ const destroy = useCallback(() => {
43
+ chunk.destroy();
44
+ }, [chunk]);
45
+
46
+ return [state, set, update, reset, destroy] as const;
47
+ }
@@ -0,0 +1,21 @@
1
+ import { useMemo } from "react";
2
+
3
+ import { useChunkValue } from "./useChunkValue";
4
+
5
+ import type { Chunk } from "../../core/core";
6
+
7
+ /**
8
+ * A hook that subscribes to a specific property of a chunk.
9
+ * This optimizes renders by only updating when the selected property changes.
10
+ */
11
+ export function useChunkProperty<T, K extends keyof T>(
12
+ chunk: Chunk<T>,
13
+ property: K
14
+ ): T[K] {
15
+ const selector = useMemo(
16
+ () => (state: T) => state[property],
17
+ [property]
18
+ );
19
+
20
+ return useChunkValue(chunk, selector);
21
+ }
@@ -0,0 +1,15 @@
1
+ import { useChunk } from "./useChunk";
2
+
3
+ import type { Chunk } from "../../core/core";
4
+
5
+ /**
6
+ * A lightweight hook that subscribes to a chunk and returns only its current value.
7
+ * Useful for read-only components that don't need to update the chunk.
8
+ */
9
+ export function useChunkValue<T, S = T>(
10
+ chunk: Chunk<T>,
11
+ selector?: (value: T) => S
12
+ ): S {
13
+ const [value] = useChunk(chunk, selector);
14
+ return value;
15
+ }
@@ -0,0 +1,35 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ import type { Chunk } from "../../core/core";
4
+
5
+ /**
6
+ * Hook to read values from multiple chunks at once.
7
+ * Only re-renders when any of the chunk values change.
8
+ */
9
+ export function useChunkValues<T extends Chunk<any>[]>(
10
+ chunks: [...T]
11
+ ): { [K in keyof T]: T[K] extends Chunk<infer U> ? U : never } {
12
+ type ReturnType = { [K in keyof T]: T[K] extends Chunk<infer U> ? U : never };
13
+
14
+ const [values, setValues] = useState<ReturnType>(() => {
15
+ return chunks.map(chunk => chunk.get()) as ReturnType;
16
+ });
17
+
18
+ useEffect(() => {
19
+ const unsubscribes = chunks.map((chunk, index) => {
20
+ return chunk.subscribe((newValue) => {
21
+ setValues(prev => {
22
+ const newValues = [...prev] as ReturnType;
23
+ newValues[index] = newValue;
24
+ return newValues;
25
+ });
26
+ });
27
+ });
28
+
29
+ return () => {
30
+ unsubscribes.forEach(unsubscribe => unsubscribe());
31
+ };
32
+ }, [chunks]);
33
+
34
+ return values;
35
+ }
@@ -0,0 +1,34 @@
1
+ import { useState, useEffect, useMemo } from "react";
2
+
3
+ import { computed, DependencyValues } from "../../core/computed";
4
+ import type { Chunk } from "../../core/core";
5
+
6
+ /**
7
+ * A hook that computes a value based on multiple chunks.
8
+ * Automatically re-computes when any dependency changes.
9
+ */
10
+ export function useComputed<TDeps extends Chunk<any>[], TResult>(
11
+ dependencies: [...TDeps],
12
+ computeFn: (...args: DependencyValues<TDeps>) => TResult
13
+ ): TResult {
14
+ // Create the computed value - memoize it based on dependencies to prevent recreation
15
+ const computedValue = useMemo(
16
+ () => computed(dependencies, computeFn),
17
+ // eslint-disable-next-line react-hooks/exhaustive-deps
18
+ [...dependencies]
19
+ );
20
+
21
+ const [state, setState] = useState<TResult>(() => computedValue.get());
22
+
23
+ useEffect(() => {
24
+ const unsubscribe = computedValue.subscribe((newValue) => {
25
+ setState(newValue);
26
+ });
27
+
28
+ return () => {
29
+ unsubscribe();
30
+ };
31
+ }, [computedValue]);
32
+
33
+ return state;
34
+ }
@@ -0,0 +1,15 @@
1
+ import { useMemo } from "react";
2
+
3
+ import { useChunk } from "./useChunk";
4
+ import type { Chunk } from "../../core/core";
5
+
6
+ /**
7
+ * A hook for creating a read-only derived value from a chunk.
8
+ * Ensures reactivity and updates when the source chunk changes.
9
+ */
10
+ export function useDerive<T, D>(chunk: Chunk<T>, fn: (value: T) => D): D {
11
+ const derivedChunk = useMemo(() => chunk.derive(fn), [chunk, fn]);
12
+ const [derivedValue] = useChunk(derivedChunk);
13
+
14
+ return derivedValue;
15
+ }
@@ -0,0 +1,9 @@
1
+ export { useChunk } from './hooks/useChunk'
2
+ export { useDerive } from './hooks/useDerive'
3
+ export { useComputed } from './hooks/useComputed'
4
+
5
+ export { useChunkValue } from './hooks/useChunkValue'
6
+ export { useChunkProperty } from './hooks/useChunkProperty'
7
+ export { useChunkValues } from './hooks/useChunkValues'
8
+
9
+ export { useAsyncChunk } from './hooks/useAsyncChunk'
package/src/utils.ts CHANGED
@@ -18,6 +18,18 @@ export function isChunk<T>(value: any): value is Chunk<T> {
18
18
  typeof value.destroy === 'function';
19
19
  }
20
20
 
21
+ export function once<T>(fn: () => T): () => T {
22
+ let called = false;
23
+ let result: T;
24
+ return () => {
25
+ if (!called) {
26
+ result = fn();
27
+ called = true;
28
+ }
29
+ return result;
30
+ };
31
+ };
32
+
21
33
  export function combineAsyncChunks<T extends Record<string, AsyncChunk<any>>>(
22
34
  chunks: T
23
35
  ): Chunk<{
package/tsconfig.json CHANGED
@@ -6,10 +6,17 @@
6
6
  "outDir": "./dist",
7
7
  "declaration": true,
8
8
  "declarationDir": "./dist/types",
9
+ "baseUrl": "./src",
10
+ "paths": {
11
+ "stunk/middleware": ["middleware"],
12
+ "stunk/react": ["use-react"]
13
+ },
9
14
  "typeRoots": ["./node_modules/@types", "./types"],
10
15
  "strict": true,
16
+ "skipLibCheck": true,
17
+ "jsx": "react",
11
18
  "esModuleInterop": true,
12
- "skipLibCheck": true
19
+ "allowSyntheticDefaultImports": true
13
20
  },
14
21
  "include": ["src/**/*", "types/stunk.d.ts"],
15
22
  "exclude": ["node_modules", "dist"]
package/types/stunk.d.ts CHANGED
@@ -4,8 +4,11 @@ declare module 'stunk' {
4
4
  export interface Chunk<T> {
5
5
  get: () => T;
6
6
  set: (value: T) => void;
7
+ update: (updater: (currentValue: T) => T) => void;
7
8
  subscribe: (callback: Subscriber<T>) => () => void;
8
- derive?: <D>(fn: (value: T) => D) => Chunk<D>;
9
+ derive: <D>(fn: (value: T) => D) => Chunk<D>;
10
+ reset: () => void;
11
+ destroy: () => void;
9
12
  }
10
13
 
11
14
  export function chunk<T>(initialValue: T): Chunk<T>;