stunk 1.2.5 → 1.4.6
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 +79 -4
- package/dist/core/asyncChunk.d.ts +22 -0
- package/dist/core/computed.d.ts +10 -0
- package/dist/core/core.d.ts +20 -0
- package/dist/core/selector.d.ts +2 -0
- package/dist/core/types.d.ts +16 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -2
- package/dist/middleware/history.d.ts +30 -0
- package/dist/middleware/index.d.ts +4 -0
- package/dist/middleware/logger.d.ts +2 -0
- package/dist/middleware/persistence.d.ts +8 -0
- package/dist/middleware/validator.d.ts +2 -0
- package/dist/use-react/hooks/useAsyncChunk.d.ts +14 -0
- package/dist/use-react/hooks/useAsyncChunk.js +27 -0
- package/dist/use-react/hooks/useChunk.d.ts +6 -0
- package/dist/use-react/hooks/useChunk.js +29 -0
- package/dist/use-react/hooks/useChunkProperty.d.ts +6 -0
- package/dist/use-react/hooks/useChunkProperty.js +10 -0
- package/dist/use-react/hooks/useChunkValue.d.ts +6 -0
- package/dist/use-react/hooks/useChunkValue.js +9 -0
- package/dist/use-react/hooks/useChunkValues.d.ts +8 -0
- package/dist/use-react/hooks/useChunkValues.js +25 -0
- package/dist/use-react/hooks/useComputed.d.ts +7 -0
- package/dist/use-react/hooks/useComputed.js +22 -0
- package/dist/use-react/hooks/useDerive.d.ts +6 -0
- package/dist/use-react/hooks/useDerive.js +11 -0
- package/dist/use-react/index.d.ts +7 -0
- package/dist/use-react/index.js +7 -0
- package/dist/utils.d.ts +14 -0
- package/package.json +22 -2
- package/src/core/computed.ts +3 -3
- package/src/index.ts +2 -2
- package/src/use-react/hooks/useAsyncChunk.ts +38 -0
- package/src/use-react/hooks/useChunk.ts +47 -0
- package/src/use-react/hooks/useChunkProperty.ts +21 -0
- package/src/use-react/hooks/useChunkValue.ts +15 -0
- package/src/use-react/hooks/useChunkValues.ts +35 -0
- package/src/use-react/hooks/useComputed.ts +34 -0
- package/src/use-react/hooks/useDerive.ts +15 -0
- package/src/use-react/index.ts +9 -0
- package/tsconfig.json +10 -3
- package/types/stunk.d.ts +0 -12
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ nameChunk.set("Olamide"); // ❌ this will throw an error, because it is a reado
|
|
|
98
98
|
|
|
99
99
|
## Batch Updates
|
|
100
100
|
|
|
101
|
-
Batch Update group multiple **state changes** together and notify **subscribers** only once at the end of the
|
|
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.
|
|
102
102
|
|
|
103
103
|
```typescript
|
|
104
104
|
import { chunk, batch } from "stunk";
|
|
@@ -152,7 +152,7 @@ console.log(fullInfoChunk.get());
|
|
|
152
152
|
// ✅ { fullName: "Ola Doe", isAdult: true }
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
|
|
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.
|
|
156
156
|
|
|
157
157
|
## Advanced Examples
|
|
158
158
|
|
|
@@ -243,7 +243,7 @@ age.set(-5); // ❌ Throws an error: "Value must be non-negative!"
|
|
|
243
243
|
|
|
244
244
|
## Time Travel (Middleware)
|
|
245
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.
|
|
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
247
|
|
|
248
248
|
```typescript
|
|
249
249
|
import { chunk } from "stunk";
|
|
@@ -277,7 +277,6 @@ const counter = withHistory(chunk(0), { maxHistory: 5 });
|
|
|
277
277
|
|
|
278
278
|
This prevents the history from growing indefinitely and ensures efficient memory usage.
|
|
279
279
|
|
|
280
|
-
|
|
281
280
|
## State Persistence (Middleware)
|
|
282
281
|
|
|
283
282
|
Stunk provides a persistence middleware to automatically save state changes to storage (localStorage, sessionStorage, etc).
|
|
@@ -315,6 +314,35 @@ const encryptedChunk = withPersistence(baseChunk, {
|
|
|
315
314
|
});
|
|
316
315
|
```
|
|
317
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
|
+
|
|
318
346
|
## Async State
|
|
319
347
|
|
|
320
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.
|
|
@@ -385,6 +413,53 @@ user.mutate(currentUser => ({
|
|
|
385
413
|
}));
|
|
386
414
|
```
|
|
387
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
|
+
|
|
388
463
|
## API Reference
|
|
389
464
|
|
|
390
465
|
### Core
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Chunk } from "./core";
|
|
2
|
+
import { AsyncChunkOpt } from "./types";
|
|
3
|
+
export interface AsyncState<T> {
|
|
4
|
+
loading: boolean;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
data: T | null;
|
|
7
|
+
}
|
|
8
|
+
export interface AsyncChunk<T> extends Chunk<AsyncState<T>> {
|
|
9
|
+
/**
|
|
10
|
+
* Reload the data from the source.
|
|
11
|
+
*/
|
|
12
|
+
reload: () => Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Mutate the data directly.
|
|
15
|
+
*/
|
|
16
|
+
mutate: (mutator: (currentData: T | null) => T) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Reset the state to the initial value.
|
|
19
|
+
*/
|
|
20
|
+
reset: () => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function asyncChunk<T>(fetcher: () => Promise<T>, options?: AsyncChunkOpt<T>): AsyncChunk<T>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Chunk } from "./core";
|
|
2
|
+
export type ChunkValue<T> = T extends Chunk<infer U> ? U : never;
|
|
3
|
+
export type DependencyValues<T extends Chunk<any>[]> = {
|
|
4
|
+
[K in keyof T]: T[K] extends Chunk<any> ? ChunkValue<T[K]> : never;
|
|
5
|
+
};
|
|
6
|
+
export interface Computed<T> extends Chunk<T> {
|
|
7
|
+
isDirty: () => boolean;
|
|
8
|
+
recompute: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function computed<TDeps extends Chunk<any>[], TResult>(dependencies: [...TDeps], computeFn: (...args: DependencyValues<TDeps>) => TResult): Computed<TResult>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type Subscriber<T> = (newValue: T) => void;
|
|
2
|
+
export type Middleware<T> = (value: T, next: (newValue: T) => void) => void;
|
|
3
|
+
export interface Chunk<T> {
|
|
4
|
+
/** Get the current value of the chunk. */
|
|
5
|
+
get: () => T;
|
|
6
|
+
/** Set a new value for the chunk. */
|
|
7
|
+
set: (value: T) => void;
|
|
8
|
+
/** Update existing value efficiently */
|
|
9
|
+
update: (updater: (currentValue: T) => T) => void;
|
|
10
|
+
/** Subscribe to changes in the chunk. Returns an unsubscribe function. */
|
|
11
|
+
subscribe: (callback: Subscriber<T>) => () => void;
|
|
12
|
+
/** Create a derived chunk based on this chunk's value. */
|
|
13
|
+
derive: <D>(fn: (value: T) => D) => Chunk<D>;
|
|
14
|
+
/** Reset the chunk to its initial value. */
|
|
15
|
+
reset: () => void;
|
|
16
|
+
/** Destroy the chunk and all its subscribers. */
|
|
17
|
+
destroy: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function batch(callback: () => void): void;
|
|
20
|
+
export declare function chunk<T>(initialValue: T, middleware?: Middleware<T>[]): Chunk<T>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AsyncChunk } from "./asyncChunk";
|
|
2
|
+
export type AsyncChunkOpt<T> = {
|
|
3
|
+
initialData?: T | null;
|
|
4
|
+
onError?: (error: Error) => void;
|
|
5
|
+
retryCount?: number;
|
|
6
|
+
retryDelay?: number;
|
|
7
|
+
};
|
|
8
|
+
export type InferAsyncData<T> = T extends AsyncChunk<infer U> ? U : never;
|
|
9
|
+
export type CombinedData<T> = {
|
|
10
|
+
[K in keyof T]: InferAsyncData<T[K]> | null;
|
|
11
|
+
};
|
|
12
|
+
export type CombinedState<T> = {
|
|
13
|
+
loading: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
data: CombinedData<T>;
|
|
16
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { chunk, batch } from './core/core';
|
|
2
|
+
export { asyncChunk } from './core/asyncChunk';
|
|
3
|
+
export { computed } from './core/computed';
|
|
4
|
+
export { select } from './core/selector';
|
|
5
|
+
export { combineAsyncChunks, once, isChunk, isValidChunkValue } from './utils';
|
|
6
|
+
export type { Chunk, Middleware } from './core/core';
|
|
7
|
+
export * as middleware from "./middleware";
|
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,30 @@
|
|
|
1
|
+
import { Chunk } from "../core/core";
|
|
2
|
+
export interface ChunkWithHistory<T> extends Chunk<T> {
|
|
3
|
+
/**
|
|
4
|
+
* Reverts to the previous state (if available).
|
|
5
|
+
*/
|
|
6
|
+
undo: () => void;
|
|
7
|
+
/**
|
|
8
|
+
* Moves to the next state (if available).
|
|
9
|
+
*/
|
|
10
|
+
redo: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if there is a previous state to revert to.
|
|
13
|
+
*/
|
|
14
|
+
canUndo: () => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Returns true if there is a next state to move to.
|
|
17
|
+
*/
|
|
18
|
+
canRedo: () => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Returns an array of all the values in the history.
|
|
21
|
+
*/
|
|
22
|
+
getHistory: () => T[];
|
|
23
|
+
/**
|
|
24
|
+
* Clears the history, keeping only the current value.
|
|
25
|
+
*/
|
|
26
|
+
clearHistory: () => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function withHistory<T>(baseChunk: Chunk<T>, options?: {
|
|
29
|
+
maxHistory?: number;
|
|
30
|
+
}): ChunkWithHistory<T>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Chunk } from "../core/core";
|
|
2
|
+
export interface PersistOptions<T> {
|
|
3
|
+
key: string;
|
|
4
|
+
storage?: Storage;
|
|
5
|
+
serialize?: (value: T) => string;
|
|
6
|
+
deserialize?: (value: string) => T;
|
|
7
|
+
}
|
|
8
|
+
export declare function withPersistence<T>(baseChunk: Chunk<T>, options: PersistOptions<T>): Chunk<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AsyncChunk, AsyncState } from "../../core/asyncChunk";
|
|
2
|
+
/**
|
|
3
|
+
* A hook that handles asynchronous state with built-in reactivity.
|
|
4
|
+
* Provides loading, error, and data states.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useAsyncChunk<T>(asyncChunk: AsyncChunk<T>): {
|
|
7
|
+
state: AsyncState<T>;
|
|
8
|
+
data: T | null;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
reload: () => Promise<void>;
|
|
12
|
+
mutate: (mutator: (currentData: T | null) => T) => void;
|
|
13
|
+
reset: () => void;
|
|
14
|
+
};
|
|
@@ -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,6 @@
|
|
|
1
|
+
import type { Chunk } from "../../core/core";
|
|
2
|
+
/**
|
|
3
|
+
* A lightweight hook that subscribes to a chunk and returns its current value, along with setters, selector, reset and destroy.
|
|
4
|
+
* Ensures reactivity and prevents unnecessary re-renders.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useChunk<T, S = T>(chunk: Chunk<T>, selector?: (value: T) => S): readonly [S, (value: T) => void, (updater: (currentValue: T) => T) => void, () => void, () => void];
|
|
@@ -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,6 @@
|
|
|
1
|
+
import type { Chunk } from "../../core/core";
|
|
2
|
+
/**
|
|
3
|
+
* A hook that subscribes to a specific property of a chunk.
|
|
4
|
+
* This optimizes renders by only updating when the selected property changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useChunkProperty<T, K extends keyof T>(chunk: Chunk<T>, property: K): T[K];
|
|
@@ -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,6 @@
|
|
|
1
|
+
import type { Chunk } from "../../core/core";
|
|
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 declare function useChunkValue<T, S = T>(chunk: Chunk<T>, selector?: (value: T) => S): S;
|
|
@@ -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,8 @@
|
|
|
1
|
+
import type { Chunk } from "../../core/core";
|
|
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 declare function useChunkValues<T extends Chunk<any>[]>(chunks: [...T]): {
|
|
7
|
+
[K in keyof T]: T[K] extends Chunk<infer U> ? U : never;
|
|
8
|
+
};
|
|
@@ -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,7 @@
|
|
|
1
|
+
import { DependencyValues } from "../../core/computed";
|
|
2
|
+
import type { Chunk } from "../../core/core";
|
|
3
|
+
/**
|
|
4
|
+
* A hook that computes a value based on multiple chunks.
|
|
5
|
+
* Automatically re-computes when any dependency changes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useComputed<TDeps extends Chunk<any>[], TResult>(dependencies: [...TDeps], computeFn: (...args: DependencyValues<TDeps>) => TResult): TResult;
|
|
@@ -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,6 @@
|
|
|
1
|
+
import type { Chunk } from "../../core/core";
|
|
2
|
+
/**
|
|
3
|
+
* A hook for creating a read-only derived value from a chunk.
|
|
4
|
+
* Ensures reactivity and updates when the source chunk changes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useDerive<T, D>(chunk: Chunk<T>, fn: (value: T) => D): D;
|
|
@@ -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';
|
|
@@ -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.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Chunk, Middleware } from "./core/core";
|
|
2
|
+
import { AsyncChunk } from "./core/asyncChunk";
|
|
3
|
+
import { InferAsyncData } from "./core/types";
|
|
4
|
+
export declare function isValidChunkValue(value: any): boolean;
|
|
5
|
+
export declare function isChunk<T>(value: any): value is Chunk<T>;
|
|
6
|
+
export declare function once<T>(fn: () => T): () => T;
|
|
7
|
+
export declare function combineAsyncChunks<T extends Record<string, AsyncChunk<any>>>(chunks: T): Chunk<{
|
|
8
|
+
loading: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
data: {
|
|
11
|
+
[K in keyof T]: InferAsyncData<T[K]> | null;
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
export declare function processMiddleware<T>(initialValue: T, middleware?: Middleware<T>[]): T;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stunk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.6",
|
|
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",
|
|
@@ -11,7 +11,21 @@
|
|
|
11
11
|
"url": "https://github.com/I-am-abdulazeez/stunk/issues"
|
|
12
12
|
},
|
|
13
13
|
"main": "dist/index.js",
|
|
14
|
-
"types": "dist/
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./middleware": {
|
|
21
|
+
"import": "./dist/middleware/index.js",
|
|
22
|
+
"types": "./dist/middleware/index.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./react": {
|
|
25
|
+
"import": "./dist/use-react/index.js",
|
|
26
|
+
"types": "./dist/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
|
}
|
package/src/core/computed.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Chunk, chunk
|
|
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/tsconfig.json
CHANGED
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
"moduleResolution": "Node",
|
|
6
6
|
"outDir": "./dist",
|
|
7
7
|
"declaration": true,
|
|
8
|
-
"declarationDir": "./dist
|
|
8
|
+
"declarationDir": "./dist",
|
|
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
|
-
"
|
|
19
|
+
"allowSyntheticDefaultImports": true
|
|
13
20
|
},
|
|
14
|
-
"include": ["src/**/*"
|
|
21
|
+
"include": ["src/**/*"],
|
|
15
22
|
"exclude": ["node_modules", "dist"]
|
|
16
23
|
}
|
package/types/stunk.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
declare module 'stunk' {
|
|
2
|
-
export type Subscriber<T> = (newValue: T) => void;
|
|
3
|
-
|
|
4
|
-
export interface Chunk<T> {
|
|
5
|
-
get: () => T;
|
|
6
|
-
set: (value: T) => void;
|
|
7
|
-
subscribe: (callback: Subscriber<T>) => () => void;
|
|
8
|
-
derive?: <D>(fn: (value: T) => D) => Chunk<D>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function chunk<T>(initialValue: T): Chunk<T>;
|
|
12
|
-
}
|