juststore 0.4.2 → 0.4.4
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/dist/form.d.ts +17 -5
- package/dist/impl.js +31 -19
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/mixed_state.js +17 -1
- package/dist/node.js +2 -2
- package/dist/path.d.ts +1 -0
- package/dist/root.js +19 -25
- package/dist/stable_keys.js +1 -1
- package/package.json +7 -14
package/dist/form.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FieldPath, FieldPathValue, FieldValues, IsEqual } from './path';
|
|
2
|
-
import type { ArrayProxy, DerivedStateProps, IsNullable, MaybeNullable, ObjectMutationMethods, ValueState } from './types';
|
|
2
|
+
import type { ArrayProxy, DerivedStateProps, IsNullable, MaybeNullable, ObjectMutationMethods, Prettify, ValueState } from './types';
|
|
3
3
|
export { createForm, useForm, type CreateFormOptions, type DeepNonNullable, type FormArrayState, type FormObjectState, type FormState, type FormStore, type FormValueState };
|
|
4
4
|
/**
|
|
5
5
|
* Common form field methods available on every form state node.
|
|
@@ -13,7 +13,12 @@ type FormCommon = {
|
|
|
13
13
|
setError: (error: string | undefined) => void;
|
|
14
14
|
};
|
|
15
15
|
type FormState<T> = IsEqual<T, unknown> extends true ? never : [NonNullable<T>] extends [readonly (infer U)[]] ? FormArrayState<U, IsNullable<T>> : [NonNullable<T>] extends [FieldValues] ? FormObjectState<NonNullable<T>, IsNullable<T>> : FormValueState<T>;
|
|
16
|
-
|
|
16
|
+
type FormReadOnlyState<T> = Prettify<Pick<FormValueState<Readonly<Required<T>>>, 'value' | 'use' | 'useCompute' | 'Render' | 'Show' | 'error' | 'setError'>>;
|
|
17
|
+
interface FormValueState<T> extends Omit<ValueState<T>, 'ensureArray' | 'ensureObject' | 'withDefault' | 'derived'>, FormCommon {
|
|
18
|
+
/** Ensure the value is an array. */
|
|
19
|
+
ensureArray(): NonNullable<T> extends (infer U)[] ? FormArrayState<U> : never;
|
|
20
|
+
/** Ensure the value is an object. */
|
|
21
|
+
ensureObject(): NonNullable<T> extends FieldValues ? FormObjectState<NonNullable<T>> : never;
|
|
17
22
|
/** Return a new state with a default value, and make the type non-nullable */
|
|
18
23
|
withDefault(defaultValue: T): FormState<NonNullable<T>>;
|
|
19
24
|
/** Virtual state derived from the current value.
|
|
@@ -30,10 +35,17 @@ interface FormValueState<T> extends Omit<ValueState<T>, 'withDefault' | 'derived
|
|
|
30
35
|
*/
|
|
31
36
|
derived: <R>({ from, to }: DerivedStateProps<T, R>) => FormState<R>;
|
|
32
37
|
}
|
|
33
|
-
type
|
|
34
|
-
|
|
38
|
+
type FormObjectProxy<T extends FieldValues> = {
|
|
39
|
+
/** Virtual state for the object's keys.
|
|
40
|
+
*
|
|
41
|
+
* This does NOT read from a real `keys` property on the stored object; it results in a stable array of keys.
|
|
42
|
+
*/
|
|
43
|
+
readonly keys: FormReadOnlyState<FieldPath<T>[]>;
|
|
44
|
+
} & {
|
|
35
45
|
[K in keyof T]-?: FormState<T[K]>;
|
|
36
|
-
}
|
|
46
|
+
};
|
|
47
|
+
type FormArrayState<T, Nullable extends boolean = false> = IsEqual<T, unknown> extends true ? never : FormValueState<MaybeNullable<T[], Nullable>> & ArrayProxy<T, FormState<T>>;
|
|
48
|
+
type FormObjectState<T extends FieldValues, Nullable extends boolean = false> = FormObjectProxy<T> & FormValueState<MaybeNullable<T, Nullable>> & ObjectMutationMethods;
|
|
37
49
|
/** Type for nested objects with proxy methods */
|
|
38
50
|
type DeepNonNullable<T> = [NonNullable<T>] extends [readonly (infer U)[]] ? U[] : [NonNullable<T>] extends [FieldValues] ? {
|
|
39
51
|
[K in keyof NonNullable<T>]-?: DeepNonNullable<NonNullable<T>[K]>;
|
package/dist/impl.js
CHANGED
|
@@ -84,7 +84,7 @@ function getNamespace(key) {
|
|
|
84
84
|
function joinPath(namespace, path) {
|
|
85
85
|
if (!path)
|
|
86
86
|
return namespace;
|
|
87
|
-
return namespace
|
|
87
|
+
return `${namespace}.${path}`;
|
|
88
88
|
}
|
|
89
89
|
function joinChildKey(parent, child) {
|
|
90
90
|
return parent ? `${parent}.${child}` : child;
|
|
@@ -93,16 +93,16 @@ function getKeyPrefixes(key) {
|
|
|
93
93
|
const dot = key.indexOf('.');
|
|
94
94
|
if (dot === -1)
|
|
95
95
|
return [];
|
|
96
|
-
const parts = key.split('.');
|
|
97
|
-
if (parts.length
|
|
96
|
+
const [first, ...parts] = key.split('.');
|
|
97
|
+
if (parts.length === 0)
|
|
98
98
|
return [];
|
|
99
99
|
const prefixes = [];
|
|
100
|
-
let current =
|
|
101
|
-
for (let i =
|
|
102
|
-
current +=
|
|
100
|
+
let current = first;
|
|
101
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
102
|
+
current += `.${parts[i]}`;
|
|
103
103
|
prefixes.push(current);
|
|
104
104
|
}
|
|
105
|
-
prefixes.unshift(
|
|
105
|
+
prefixes.unshift(first);
|
|
106
106
|
return prefixes;
|
|
107
107
|
}
|
|
108
108
|
/** Snapshot getter used by React's useSyncExternalStore. */
|
|
@@ -260,7 +260,7 @@ function setNestedValue(obj, path, value) {
|
|
|
260
260
|
}
|
|
261
261
|
else if (typeof current === 'object' && current !== null) {
|
|
262
262
|
const currentObj = current;
|
|
263
|
-
const hadKey = Object.
|
|
263
|
+
const hadKey = Object.hasOwn(currentObj, lastSegment);
|
|
264
264
|
if (value === undefined) {
|
|
265
265
|
delete currentObj[lastSegment];
|
|
266
266
|
if (hadKey) {
|
|
@@ -316,21 +316,29 @@ function notifyListeners(key, oldValue, newValue, { skipRoot = false, skipChildr
|
|
|
316
316
|
// exact match only
|
|
317
317
|
const listenerSet = listeners.get(key);
|
|
318
318
|
if (listenerSet) {
|
|
319
|
-
listenerSet.forEach(listener =>
|
|
319
|
+
listenerSet.forEach(listener => {
|
|
320
|
+
listener();
|
|
321
|
+
});
|
|
320
322
|
}
|
|
321
323
|
return;
|
|
322
324
|
}
|
|
323
325
|
// Exact key match
|
|
324
326
|
const exactSet = listeners.get(key);
|
|
325
327
|
if (exactSet) {
|
|
326
|
-
exactSet.forEach(listener =>
|
|
328
|
+
exactSet.forEach(listener => {
|
|
329
|
+
listener();
|
|
330
|
+
});
|
|
327
331
|
}
|
|
328
332
|
// Ancestor keys match (including namespace root)
|
|
329
333
|
if (!skipRoot) {
|
|
330
334
|
const namespace = getNamespace(key);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
335
|
+
if (namespace !== key) {
|
|
336
|
+
const rootSet = listeners.get(namespace);
|
|
337
|
+
if (rootSet) {
|
|
338
|
+
rootSet.forEach(listener => {
|
|
339
|
+
listener();
|
|
340
|
+
});
|
|
341
|
+
}
|
|
334
342
|
}
|
|
335
343
|
// Also notify intermediate ancestors
|
|
336
344
|
const prefixes = getKeyPrefixes(key);
|
|
@@ -339,7 +347,9 @@ function notifyListeners(key, oldValue, newValue, { skipRoot = false, skipChildr
|
|
|
339
347
|
continue; // Already handled
|
|
340
348
|
const prefixSet = listeners.get(prefix);
|
|
341
349
|
if (prefixSet) {
|
|
342
|
-
prefixSet.forEach(listener =>
|
|
350
|
+
prefixSet.forEach(listener => {
|
|
351
|
+
listener();
|
|
352
|
+
});
|
|
343
353
|
}
|
|
344
354
|
}
|
|
345
355
|
}
|
|
@@ -369,7 +379,9 @@ function notifyListeners(key, oldValue, newValue, { skipRoot = false, skipChildr
|
|
|
369
379
|
if (forceNotify || !isEqual(oldChildValue, newChildValue)) {
|
|
370
380
|
const childSet = listeners.get(childKey);
|
|
371
381
|
if (childSet) {
|
|
372
|
-
childSet.forEach(listener =>
|
|
382
|
+
childSet.forEach(listener => {
|
|
383
|
+
listener();
|
|
384
|
+
});
|
|
373
385
|
}
|
|
374
386
|
}
|
|
375
387
|
}
|
|
@@ -395,13 +407,13 @@ function subscribe(key, listener) {
|
|
|
395
407
|
if (!listeners.has(key)) {
|
|
396
408
|
listeners.set(key, new Set());
|
|
397
409
|
}
|
|
398
|
-
listeners.get(key)
|
|
410
|
+
listeners.get(key)?.add(listener);
|
|
399
411
|
const prefixes = getKeyPrefixes(key);
|
|
400
412
|
for (const prefix of prefixes) {
|
|
401
413
|
if (!descendantListenerKeysByPrefix.has(prefix)) {
|
|
402
414
|
descendantListenerKeysByPrefix.set(prefix, new Set());
|
|
403
415
|
}
|
|
404
|
-
descendantListenerKeysByPrefix.get(prefix)
|
|
416
|
+
descendantListenerKeysByPrefix.get(prefix)?.add(key);
|
|
405
417
|
}
|
|
406
418
|
return () => {
|
|
407
419
|
const keyListeners = listeners.get(key);
|
|
@@ -471,12 +483,12 @@ function rename(path, oldKey, newKey, memoryOnly) {
|
|
|
471
483
|
const obj = current;
|
|
472
484
|
if (oldKey === newKey)
|
|
473
485
|
return;
|
|
474
|
-
if (!Object.
|
|
486
|
+
if (!Object.hasOwn(obj, oldKey))
|
|
475
487
|
return;
|
|
476
488
|
const keyOrder = getStableKeys(obj);
|
|
477
489
|
const entries = [];
|
|
478
490
|
for (const key of keyOrder) {
|
|
479
|
-
if (!Object.
|
|
491
|
+
if (!Object.hasOwn(obj, key))
|
|
480
492
|
continue;
|
|
481
493
|
if (key === oldKey) {
|
|
482
494
|
entries.push([newKey, obj[oldKey]]);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { type Atom, createAtom } from './atom';
|
|
2
2
|
export type * from './form';
|
|
3
3
|
export { useForm } from './form';
|
|
4
|
-
export {
|
|
4
|
+
export { isEqual } from './impl';
|
|
5
|
+
export { createMemoryStore, type MemoryStore, useMemoryStore } from './memory';
|
|
5
6
|
export { createMixedState } from './mixed_state';
|
|
6
7
|
export type * from './path';
|
|
7
8
|
export { createStore, type Store } from './store';
|
package/dist/index.js
CHANGED
package/dist/mixed_state.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { isEqual } from './impl';
|
|
1
3
|
export { createMixedState };
|
|
2
4
|
/**
|
|
3
5
|
* Creates a mixed state that combines multiple states into a tuple.
|
|
@@ -22,7 +24,21 @@ function createMixedState(...states) {
|
|
|
22
24
|
},
|
|
23
25
|
use,
|
|
24
26
|
useCompute(fn) {
|
|
25
|
-
|
|
27
|
+
const [value, setValue] = useState(fn(this.value));
|
|
28
|
+
const recompute = useCallback(() => {
|
|
29
|
+
const newValue = fn(this.value);
|
|
30
|
+
// skip update if the new value is the same as the previous value
|
|
31
|
+
setValue(prev => (isEqual(prev, newValue) ? prev : newValue));
|
|
32
|
+
}, [fn]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const unsubscribeFns = states.map(state => state.subscribe(recompute));
|
|
35
|
+
return () => {
|
|
36
|
+
unsubscribeFns.forEach(unsubscribe => {
|
|
37
|
+
unsubscribe();
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return value;
|
|
26
42
|
},
|
|
27
43
|
Render({ children }) {
|
|
28
44
|
const value = use();
|
package/dist/node.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: <T is not known at this point> */
|
|
2
|
+
/** biome-ignore-all lint/suspicious/noAssignInExpressions: <getter methods are cached> */
|
|
2
3
|
import { getStableKeys } from './impl';
|
|
3
4
|
export { createNode, createRootNode };
|
|
4
5
|
/**
|
|
@@ -350,7 +351,6 @@ function createKeysNode(storeApi, path, getObjectValue) {
|
|
|
350
351
|
return (_target._Render ??= ({ children }) => children(storeApi.useCompute(signalPath, computeKeys), () => { }));
|
|
351
352
|
}
|
|
352
353
|
if (prop === 'Show') {
|
|
353
|
-
// eslint-disable-next-line react/display-name
|
|
354
354
|
return (_target._Show ??= ({ children, on }) => {
|
|
355
355
|
const show = storeApi.useCompute(signalPath, () => on(computeKeys()), [on]);
|
|
356
356
|
return show ? children : null;
|
package/dist/path.d.ts
CHANGED
package/dist/root.js
CHANGED
|
@@ -15,6 +15,7 @@ export { createStoreRoot };
|
|
|
15
15
|
* @returns A proxy object providing both path-based and dynamic property access to the store
|
|
16
16
|
*/
|
|
17
17
|
function createStoreRoot(namespace, defaultValue, options = {}) {
|
|
18
|
+
'use memo';
|
|
18
19
|
const memoryOnly = options?.memoryOnly ?? false;
|
|
19
20
|
// merge with default value and save in memory only
|
|
20
21
|
produce(namespace, { ...defaultValue, ...(getSnapshot(namespace, memoryOnly) ?? {}) }, true, true);
|
|
@@ -45,16 +46,21 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
|
|
|
45
46
|
const fnRef = useRef(fn);
|
|
46
47
|
fnRef.current = fn;
|
|
47
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
|
+
}
|
|
48
60
|
const subscribeToPath = useCallback((onStoreChange) => subscribe(fullPath, onStoreChange), [fullPath]);
|
|
49
61
|
const getComputedSnapshot = useCallback(() => {
|
|
50
|
-
if (cacheRef.current && cacheRef.current.path !== fullPath) {
|
|
51
|
-
cacheRef.current = null;
|
|
52
|
-
}
|
|
53
|
-
if (cacheRef.current && !isEqual(cacheRef.current.deps, deps)) {
|
|
54
|
-
cacheRef.current = null;
|
|
55
|
-
}
|
|
56
62
|
const storeValue = getSnapshot(fullPath, memoryOnly);
|
|
57
|
-
if (cacheRef.current &&
|
|
63
|
+
if (cacheRef.current && Object.is(cacheRef.current.storeValue, storeValue)) {
|
|
58
64
|
// same store value, return the same computed value
|
|
59
65
|
return cacheRef.current.computed;
|
|
60
66
|
}
|
|
@@ -66,9 +72,9 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
|
|
|
66
72
|
cacheRef.current.storeValue = storeValue;
|
|
67
73
|
return cacheRef.current.computed;
|
|
68
74
|
}
|
|
69
|
-
cacheRef.current = {
|
|
75
|
+
cacheRef.current = { storeValue, computed: computedNext };
|
|
70
76
|
return computedNext;
|
|
71
|
-
}, [fullPath
|
|
77
|
+
}, [fullPath]);
|
|
72
78
|
return useSyncExternalStore(subscribeToPath, getComputedSnapshot, getComputedSnapshot);
|
|
73
79
|
},
|
|
74
80
|
notify: (path) => {
|
|
@@ -80,31 +86,19 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
|
|
|
80
86
|
});
|
|
81
87
|
},
|
|
82
88
|
useState: (path) => {
|
|
83
|
-
const fullPath = joinPath(namespace, path);
|
|
84
89
|
const setValue = useCallback((value) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const newValue = value(currentValue);
|
|
88
|
-
return setLeaf(namespace, path, newValue, false, memoryOnly);
|
|
89
|
-
}
|
|
90
|
-
return setLeaf(namespace, path, value, false, memoryOnly);
|
|
91
|
-
}, [fullPath, path]);
|
|
90
|
+
storeApi.set(path, value, false);
|
|
91
|
+
}, [path]);
|
|
92
92
|
return [
|
|
93
93
|
useObject(namespace, path, memoryOnly),
|
|
94
94
|
setValue
|
|
95
95
|
];
|
|
96
96
|
},
|
|
97
97
|
Render: ({ path, children }) => {
|
|
98
|
-
const fullPath = joinPath(namespace, path);
|
|
99
98
|
const value = useObject(namespace, path, memoryOnly);
|
|
100
99
|
const update = useCallback((value) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const newValue = value(currentValue);
|
|
104
|
-
return setLeaf(namespace, path, newValue, false, memoryOnly);
|
|
105
|
-
}
|
|
106
|
-
return setLeaf(namespace, path, value, false, memoryOnly);
|
|
107
|
-
}, [fullPath, path]);
|
|
100
|
+
storeApi.set(path, value, false);
|
|
101
|
+
}, [path]);
|
|
108
102
|
return children(value, update);
|
|
109
103
|
},
|
|
110
104
|
Show: ({ path, children, on }) => {
|
package/dist/stable_keys.js
CHANGED
|
@@ -13,7 +13,7 @@ function getStableKeys(value) {
|
|
|
13
13
|
const target = value;
|
|
14
14
|
const existing = externalKeyOrder.get(target);
|
|
15
15
|
if (existing) {
|
|
16
|
-
const next = existing.filter(k => Object.
|
|
16
|
+
const next = existing.filter(k => Object.hasOwn(target, k));
|
|
17
17
|
const nextSet = new Set(next);
|
|
18
18
|
for (const k of Object.keys(target)) {
|
|
19
19
|
if (nextSet.has(k))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "juststore",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "A small, expressive, and type-safe state management library for React.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"react hooks"
|
|
36
36
|
],
|
|
37
37
|
"scripts": {
|
|
38
|
-
"build": "bun
|
|
39
|
-
"typecheck": "bun
|
|
40
|
-
"lint": "bun
|
|
41
|
-
"format": "bun
|
|
42
|
-
"format:check": "bun
|
|
38
|
+
"build": "bun --bun tsc",
|
|
39
|
+
"typecheck": "bun --bun tsc --noEmit",
|
|
40
|
+
"lint": "bun --bun biome",
|
|
41
|
+
"format": "bun --bun biome format --write",
|
|
42
|
+
"format:check": "bun --bun biome format",
|
|
43
43
|
"prepublishOnly": "bun run build",
|
|
44
44
|
"publish": "npm publish --access public",
|
|
45
45
|
"prepare": "husky"
|
|
@@ -52,18 +52,11 @@
|
|
|
52
52
|
"react-fast-compare": "^3.2.2"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "^2.3.14",
|
|
55
56
|
"@eslint/js": "^9.39.2",
|
|
56
57
|
"@types/node": "^24.10.4",
|
|
57
58
|
"@types/react": "^19.2.7",
|
|
58
|
-
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
59
|
-
"@typescript-eslint/parser": "^8.50.1",
|
|
60
|
-
"eslint": "^9.39.2",
|
|
61
|
-
"eslint-plugin-prettier": "^5.5.4",
|
|
62
|
-
"eslint-plugin-react": "^7.37.5",
|
|
63
|
-
"eslint-plugin-react-hooks": "^7.0.1",
|
|
64
|
-
"eslint-plugin-react-refresh": "^0.4.26",
|
|
65
59
|
"husky": "^9.1.7",
|
|
66
|
-
"prettier": "^3.7.4",
|
|
67
60
|
"react": "^19.2.3",
|
|
68
61
|
"typescript": "^5.9.3"
|
|
69
62
|
}
|