juststore 1.0.1 → 1.1.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 +5 -4
- package/dist/atom.d.ts +2 -0
- package/dist/atom.js +4 -1
- package/dist/impl.d.ts +2 -1
- package/dist/impl.js +38 -2
- package/dist/root.js +3 -36
- package/dist/utils.d.ts +29 -8
- package/dist/utils.js +24 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -385,7 +385,7 @@ import { Conditional, Render, RenderWithUpdate } from "juststore";
|
|
|
385
385
|
</RenderWithUpdate>;
|
|
386
386
|
|
|
387
387
|
<Conditional state={store.user.role} on={(role) => role === "admin"}>
|
|
388
|
-
|
|
388
|
+
<AdminPage />
|
|
389
389
|
</Conditional>;
|
|
390
390
|
```
|
|
391
391
|
|
|
@@ -400,7 +400,7 @@ import { Conditional, Render, RenderWithUpdate } from "juststore";
|
|
|
400
400
|
- `useForm(defaultValue, fieldConfigs?)`
|
|
401
401
|
- `createMixedState(...states)`
|
|
402
402
|
- `createAtom(id, defaultValue, persistent?)`
|
|
403
|
-
- `Render`, `RenderWithUpdate`, `Conditional`
|
|
403
|
+
- `Render`, `RenderWithUpdate`, `Conditional`, `ConditionalRender`
|
|
404
404
|
- `isEqual`
|
|
405
405
|
- All public types from `path`, `types`, and `form`
|
|
406
406
|
|
|
@@ -429,7 +429,7 @@ Creates memory-only stores (no localStorage persistence).
|
|
|
429
429
|
Creates a scalar atom-like state.
|
|
430
430
|
|
|
431
431
|
- `persistent` defaults to `false`
|
|
432
|
-
- methods: `.value`, `.use()`, `.set(value | updater)`, `.reset()`, `.subscribe(listener)`
|
|
432
|
+
- methods: `.value`, `.use()`, `.set(value | updater)`, `.reset()`, `.subscribe(listener)`, `.useCompute(fn, deps?)`
|
|
433
433
|
|
|
434
434
|
### `createForm(namespace, defaultValue, fieldConfigs?)` / `useForm(defaultValue, fieldConfigs?)`
|
|
435
435
|
|
|
@@ -461,7 +461,8 @@ Combines multiple states into one read-only tuple-like state.
|
|
|
461
461
|
|
|
462
462
|
- `Render` - render-prop helper for read-only usage
|
|
463
463
|
- `RenderWithUpdate` - render-prop helper with updater callback
|
|
464
|
-
- `Conditional` -
|
|
464
|
+
- `Conditional` - show/hide children based on predicate; uses `Activity` so children stay mounted when hidden (state preserved)
|
|
465
|
+
- `ConditionalRender` - render only when predicate is true; children are a render prop receiving the value; returns `null` when false (unmounted)
|
|
465
466
|
|
|
466
467
|
## Store / State Methods
|
|
467
468
|
|
package/dist/atom.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ type Atom<T> = {
|
|
|
16
16
|
reset: () => void;
|
|
17
17
|
/** Subscribe to the value.with a callback function. */
|
|
18
18
|
subscribe: (listener: (value: T) => void) => () => void;
|
|
19
|
+
/** Compute a derived value from the current value, similar to useState + useMemo */
|
|
20
|
+
useCompute: <R>(fn: (value: T) => R, deps?: readonly unknown[]) => R;
|
|
19
21
|
};
|
|
20
22
|
type AtomSetState<T> = (value: T | ((prev: T) => T)) => void;
|
|
21
23
|
/**
|
package/dist/atom.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useSyncExternalStore } from 'react';
|
|
2
|
-
import { getSnapshot, updateSnapshot } from './impl';
|
|
2
|
+
import { getSnapshot, updateSnapshot, useCompute } from './impl';
|
|
3
3
|
export { createAtom };
|
|
4
4
|
/**
|
|
5
5
|
* Creates an atom with a given id and default value.
|
|
@@ -53,6 +53,9 @@ function createAtom(id, defaultValue, persistent = false) {
|
|
|
53
53
|
if (prop === 'subscribe') {
|
|
54
54
|
return (target._subscribe ??= (listener) => subscribeAtom(key, memoryOnly, listener));
|
|
55
55
|
}
|
|
56
|
+
if (prop === 'useCompute') {
|
|
57
|
+
return (target._useCompute ??= (fn, deps) => useCompute(key, undefined, fn, deps, memoryOnly));
|
|
58
|
+
}
|
|
56
59
|
return undefined;
|
|
57
60
|
}
|
|
58
61
|
});
|
package/dist/impl.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FieldPath, FieldPathValue, FieldValues } from './path';
|
|
2
2
|
import { getStableKeys, setExternalKeyOrder } from './stable_keys';
|
|
3
|
-
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useDebounce, useObject };
|
|
3
|
+
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useDebounce, useObject, useCompute };
|
|
4
4
|
declare function testReset(): void;
|
|
5
5
|
declare function isClass(value: unknown): boolean;
|
|
6
6
|
declare function isRecord(value: unknown): boolean;
|
|
@@ -64,6 +64,7 @@ declare function notifyListeners(key: string, oldValue: unknown, newValue: unkno
|
|
|
64
64
|
* @returns An unsubscribe function to remove the listener
|
|
65
65
|
*/
|
|
66
66
|
declare function subscribe(key: string, listener: () => void): () => void;
|
|
67
|
+
declare function useCompute<T = unknown, R = unknown>(namespace: string, path: string | undefined, fn: (value: T) => R, deps?: readonly unknown[], memoryOnly?: boolean): R;
|
|
67
68
|
/**
|
|
68
69
|
* Core mutation function that updates the store and notifies listeners.
|
|
69
70
|
*
|
package/dist/impl.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
2
|
import rfcIsEqual from 'react-fast-compare';
|
|
3
3
|
import { KVStore } from './kv_store';
|
|
4
4
|
import { getExternalKeyOrder, getStableKeys, setExternalKeyOrder } from './stable_keys';
|
|
5
|
-
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useDebounce, useObject };
|
|
5
|
+
export { getNestedValue, getSnapshot, getStableKeys, isClass, isEqual, isRecord, joinPath, notifyListeners, produce, rename, setExternalKeyOrder, setLeaf, setNestedValue, subscribe, testReset, updateSnapshot, useDebounce, useObject, useCompute };
|
|
6
6
|
const inMemStorage = new Map();
|
|
7
7
|
const listeners = new Map();
|
|
8
8
|
const descendantListenerKeysByPrefix = new Map();
|
|
@@ -434,6 +434,42 @@ function subscribe(key, listener) {
|
|
|
434
434
|
}
|
|
435
435
|
};
|
|
436
436
|
}
|
|
437
|
+
function useCompute(namespace, path, fn, deps, memoryOnly = false) {
|
|
438
|
+
const fullPath = joinPath(namespace, path);
|
|
439
|
+
const fnRef = useRef(fn);
|
|
440
|
+
fnRef.current = fn;
|
|
441
|
+
const cacheRef = useRef(null);
|
|
442
|
+
const depsRef = useRef(deps);
|
|
443
|
+
// Invalidate cached compute when hook inputs change.
|
|
444
|
+
if (!isEqual(depsRef.current, deps)) {
|
|
445
|
+
depsRef.current = deps;
|
|
446
|
+
cacheRef.current = null;
|
|
447
|
+
}
|
|
448
|
+
const pathRef = useRef(fullPath);
|
|
449
|
+
if (pathRef.current !== fullPath) {
|
|
450
|
+
pathRef.current = fullPath;
|
|
451
|
+
cacheRef.current = null;
|
|
452
|
+
}
|
|
453
|
+
const subscribeToPath = useCallback((onStoreChange) => subscribe(fullPath, onStoreChange), [fullPath]);
|
|
454
|
+
const getComputedSnapshot = useCallback(() => {
|
|
455
|
+
const storeValue = getSnapshot(fullPath, memoryOnly);
|
|
456
|
+
if (cacheRef.current && Object.is(cacheRef.current.storeValue, storeValue)) {
|
|
457
|
+
// same store value, return the same computed value
|
|
458
|
+
return cacheRef.current.computed;
|
|
459
|
+
}
|
|
460
|
+
const computedNext = fnRef.current(storeValue);
|
|
461
|
+
// Important: even if storeValue changed, we should avoid forcing a re-render
|
|
462
|
+
// when the computed result is logically unchanged. `useSyncExternalStore`
|
|
463
|
+
// uses `Object.is` on the snapshot; returning the same reference will bail out.
|
|
464
|
+
if (cacheRef.current && isEqual(cacheRef.current.computed, computedNext)) {
|
|
465
|
+
cacheRef.current.storeValue = storeValue;
|
|
466
|
+
return cacheRef.current.computed;
|
|
467
|
+
}
|
|
468
|
+
cacheRef.current = { storeValue, computed: computedNext };
|
|
469
|
+
return computedNext;
|
|
470
|
+
}, [fullPath, memoryOnly]);
|
|
471
|
+
return useSyncExternalStore(subscribeToPath, getComputedSnapshot, getComputedSnapshot);
|
|
472
|
+
}
|
|
437
473
|
/**
|
|
438
474
|
* Core mutation function that updates the store and notifies listeners.
|
|
439
475
|
*
|
package/dist/root.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useCallback
|
|
2
|
-
import { getNestedValue, getSnapshot,
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { getNestedValue, getSnapshot, joinPath, notifyListeners, produce, rename, setLeaf, subscribe, useCompute, useDebounce, useObject } from './impl';
|
|
3
3
|
import { createRootNode } from './node';
|
|
4
4
|
export { createStoreRoot };
|
|
5
5
|
/**
|
|
@@ -42,40 +42,7 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
|
|
|
42
42
|
return unsubscribe;
|
|
43
43
|
},
|
|
44
44
|
useCompute: (path, fn, deps) => {
|
|
45
|
-
|
|
46
|
-
const fnRef = useRef(fn);
|
|
47
|
-
fnRef.current = fn;
|
|
48
|
-
const cacheRef = useRef(null);
|
|
49
|
-
const depsRef = useRef(deps);
|
|
50
|
-
// Invalidate cached compute when hook inputs change.
|
|
51
|
-
if (!isEqual(depsRef.current, deps)) {
|
|
52
|
-
depsRef.current = deps;
|
|
53
|
-
cacheRef.current = null;
|
|
54
|
-
}
|
|
55
|
-
const pathRef = useRef(fullPath);
|
|
56
|
-
if (pathRef.current !== fullPath) {
|
|
57
|
-
pathRef.current = fullPath;
|
|
58
|
-
cacheRef.current = null;
|
|
59
|
-
}
|
|
60
|
-
const subscribeToPath = useCallback((onStoreChange) => subscribe(fullPath, onStoreChange), [fullPath]);
|
|
61
|
-
const getComputedSnapshot = useCallback(() => {
|
|
62
|
-
const storeValue = getSnapshot(fullPath, memoryOnly);
|
|
63
|
-
if (cacheRef.current && Object.is(cacheRef.current.storeValue, storeValue)) {
|
|
64
|
-
// same store value, return the same computed value
|
|
65
|
-
return cacheRef.current.computed;
|
|
66
|
-
}
|
|
67
|
-
const computedNext = fnRef.current(storeValue);
|
|
68
|
-
// Important: even if storeValue changed, we should avoid forcing a re-render
|
|
69
|
-
// when the computed result is logically unchanged. `useSyncExternalStore`
|
|
70
|
-
// uses `Object.is` on the snapshot; returning the same reference will bail out.
|
|
71
|
-
if (cacheRef.current && isEqual(cacheRef.current.computed, computedNext)) {
|
|
72
|
-
cacheRef.current.storeValue = storeValue;
|
|
73
|
-
return cacheRef.current.computed;
|
|
74
|
-
}
|
|
75
|
-
cacheRef.current = { storeValue, computed: computedNext };
|
|
76
|
-
return computedNext;
|
|
77
|
-
}, [fullPath]);
|
|
78
|
-
return useSyncExternalStore(subscribeToPath, getComputedSnapshot, getComputedSnapshot);
|
|
45
|
+
return useCompute(namespace, path, fn, deps, memoryOnly);
|
|
79
46
|
},
|
|
80
47
|
notify: (path) => {
|
|
81
48
|
const value = getNestedValue(getSnapshot(namespace, memoryOnly), path);
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Atom } from './atom';
|
|
2
2
|
import type { StoreSetStateValue, ValueState } from './types';
|
|
3
|
-
export { Render, RenderWithUpdate, Conditional };
|
|
3
|
+
export { Render, RenderWithUpdate, Conditional, ConditionalRender };
|
|
4
4
|
type AtomLike<T> = Pick<Atom<T> | ValueState<T>, 'use' | 'set' | 'value'>;
|
|
5
|
-
type ReadOnlyAtomLike<T> = Pick<Atom<T> | ValueState<T>, 'use' | 'value'>;
|
|
5
|
+
type ReadOnlyAtomLike<T> = Pick<Atom<T> | ValueState<T>, 'use' | 'useCompute' | 'value'>;
|
|
6
6
|
type RenderProps<State extends ReadOnlyAtomLike<unknown>> = {
|
|
7
7
|
state: State;
|
|
8
8
|
children: (value: State['value']) => React.ReactNode;
|
|
@@ -11,6 +11,16 @@ type RenderWithUpdateProps<State extends AtomLike<unknown>> = {
|
|
|
11
11
|
state: State;
|
|
12
12
|
children: (value: State['value'], update: (value: StoreSetStateValue<State['value']>) => void) => React.ReactNode;
|
|
13
13
|
};
|
|
14
|
+
type ConditionalProps<State extends ReadOnlyAtomLike<unknown>> = {
|
|
15
|
+
state: State;
|
|
16
|
+
on: (value: State['value']) => boolean;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
};
|
|
19
|
+
type ConditionalRenderProps<State extends ReadOnlyAtomLike<unknown>> = {
|
|
20
|
+
state: State;
|
|
21
|
+
on: (value: State['value']) => boolean;
|
|
22
|
+
children: (value: State['value']) => React.ReactNode;
|
|
23
|
+
};
|
|
14
24
|
/**
|
|
15
25
|
* Renders the provided children function with the current value from the state.
|
|
16
26
|
*
|
|
@@ -34,18 +44,29 @@ declare function Render<State extends ReadOnlyAtomLike<unknown>>({ state, childr
|
|
|
34
44
|
* @returns The result of calling children with the current value and update function.
|
|
35
45
|
*/
|
|
36
46
|
declare function RenderWithUpdate<State extends AtomLike<unknown>>({ state, children }: RenderWithUpdateProps<State>): import("react").ReactNode;
|
|
47
|
+
/**
|
|
48
|
+
* Conditionally shows or hides the children based on the result of the `on` predicate.
|
|
49
|
+
*
|
|
50
|
+
* It uses the Activity component to keep component states even when hidden.
|
|
51
|
+
*
|
|
52
|
+
* @template T The type of the state value.
|
|
53
|
+
* @param props - The props object.
|
|
54
|
+
* @param props.state - The ValueState whose value will be used.
|
|
55
|
+
* @param props.on - A predicate that receives the value and returns whether to show children.
|
|
56
|
+
* @param props.children - The component to render if the predicate returns true.
|
|
57
|
+
* @returns The Activity component with the children.
|
|
58
|
+
*/
|
|
59
|
+
declare function Conditional<State extends ReadOnlyAtomLike<unknown>>({ state, on, children }: ConditionalProps<State>): import("react/jsx-runtime").JSX.Element;
|
|
37
60
|
/**
|
|
38
61
|
* Conditionally renders the children function based on the result of the `on` predicate.
|
|
39
62
|
*
|
|
63
|
+
* It returns null if the predicate returns false.
|
|
64
|
+
*
|
|
40
65
|
* @template T The type of the state value.
|
|
41
66
|
* @param props - The props object.
|
|
42
67
|
* @param props.state - The ValueState whose value will be used.
|
|
43
68
|
* @param props.on - A predicate that receives the value and returns whether to show children.
|
|
44
|
-
* @param props.children -
|
|
69
|
+
* @param props.children - The render function that receives the value.
|
|
45
70
|
* @returns The result of children if the predicate returns true, otherwise null.
|
|
46
71
|
*/
|
|
47
|
-
declare function
|
|
48
|
-
state: State;
|
|
49
|
-
on: (value: State['value']) => boolean;
|
|
50
|
-
children: (value: State['value']) => React.ReactNode;
|
|
51
|
-
}): import("react").ReactNode;
|
|
72
|
+
declare function ConditionalRender<State extends ReadOnlyAtomLike<unknown>>({ state, on, children }: ConditionalRenderProps<State>): import("react").ReactNode;
|
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Activity, useCallback } from 'react';
|
|
3
|
+
export { Render, RenderWithUpdate, Conditional, ConditionalRender };
|
|
3
4
|
/**
|
|
4
5
|
* Renders the provided children function with the current value from the state.
|
|
5
6
|
*
|
|
@@ -37,19 +38,37 @@ function RenderWithUpdate({ state, children }) {
|
|
|
37
38
|
}, [state]);
|
|
38
39
|
return children(value, update);
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Conditionally shows or hides the children based on the result of the `on` predicate.
|
|
43
|
+
*
|
|
44
|
+
* It uses the Activity component to keep component states even when hidden.
|
|
45
|
+
*
|
|
46
|
+
* @template T The type of the state value.
|
|
47
|
+
* @param props - The props object.
|
|
48
|
+
* @param props.state - The ValueState whose value will be used.
|
|
49
|
+
* @param props.on - A predicate that receives the value and returns whether to show children.
|
|
50
|
+
* @param props.children - The component to render if the predicate returns true.
|
|
51
|
+
* @returns The Activity component with the children.
|
|
52
|
+
*/
|
|
53
|
+
function Conditional({ state, on, children }) {
|
|
54
|
+
const show = state.useCompute(on);
|
|
55
|
+
return _jsx(Activity, { mode: show ? 'visible' : 'hidden', children: children });
|
|
56
|
+
}
|
|
40
57
|
/**
|
|
41
58
|
* Conditionally renders the children function based on the result of the `on` predicate.
|
|
42
59
|
*
|
|
60
|
+
* It returns null if the predicate returns false.
|
|
61
|
+
*
|
|
43
62
|
* @template T The type of the state value.
|
|
44
63
|
* @param props - The props object.
|
|
45
64
|
* @param props.state - The ValueState whose value will be used.
|
|
46
65
|
* @param props.on - A predicate that receives the value and returns whether to show children.
|
|
47
|
-
* @param props.children -
|
|
66
|
+
* @param props.children - The render function that receives the value.
|
|
48
67
|
* @returns The result of children if the predicate returns true, otherwise null.
|
|
49
68
|
*/
|
|
50
|
-
function
|
|
69
|
+
function ConditionalRender({ state, on, children }) {
|
|
51
70
|
const value = state.use();
|
|
52
|
-
const show = on(value);
|
|
71
|
+
const show = on(value);
|
|
53
72
|
if (!show) {
|
|
54
73
|
return null;
|
|
55
74
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "juststore",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A small, expressive, and type-safe state management library for React.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "bun --bun tsc",
|
|
29
29
|
"typecheck": "bun --bun tsc --noEmit",
|
|
30
|
-
"lint": "
|
|
31
|
-
"format": "
|
|
32
|
-
"format:check": "
|
|
30
|
+
"lint": "biome check",
|
|
31
|
+
"format": "biome format --write",
|
|
32
|
+
"format:check": "biome format",
|
|
33
33
|
"prepublishOnly": "bun run build",
|
|
34
34
|
"publish": "npm publish --access public",
|
|
35
35
|
"prepare": "husky"
|