juststore 1.0.1 → 1.1.1

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
@@ -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
- {(role) => <div>{role}</div>}
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` - conditional render helper based on predicate
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, useRef, useSyncExternalStore } from 'react';
2
- import { getNestedValue, getSnapshot, isEqual, joinPath, notifyListeners, produce, rename, setLeaf, subscribe, useDebounce, useObject } from './impl';
1
+ import { useCallback } from 'react';
2
+ import { getNestedValue, getSnapshot, isRecord, joinPath, notifyListeners, produce, rename, setLeaf, subscribe, useCompute, useDebounce, useObject } from './impl';
3
3
  import { createRootNode } from './node';
4
4
  export { createStoreRoot };
5
5
  /**
@@ -18,7 +18,7 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
18
18
  'use memo';
19
19
  const memoryOnly = options?.memoryOnly ?? false;
20
20
  // merge with default value and save in memory only
21
- produce(namespace, { ...defaultValue, ...(getSnapshot(namespace, memoryOnly) ?? {}) }, true, true);
21
+ produce(namespace, mergeWithDefaults(defaultValue, getSnapshot(namespace, memoryOnly)), true, true);
22
22
  const storeApi = {
23
23
  state: (path) => createRootNode(storeApi, path),
24
24
  use: (path) => useObject(namespace, path, memoryOnly),
@@ -42,40 +42,7 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
42
42
  return unsubscribe;
43
43
  },
44
44
  useCompute: (path, fn, deps) => {
45
- const fullPath = joinPath(namespace, path);
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);
@@ -97,3 +64,18 @@ function createStoreRoot(namespace, defaultValue, options = {}) {
97
64
  };
98
65
  return storeApi;
99
66
  }
67
+ function mergeWithDefaults(defaultValue, existingValue) {
68
+ if (existingValue === undefined) {
69
+ return defaultValue;
70
+ }
71
+ if (!isRecord(defaultValue) || !isRecord(existingValue)) {
72
+ return existingValue;
73
+ }
74
+ const defaults = defaultValue;
75
+ const existing = existingValue;
76
+ const merged = { ...existing };
77
+ for (const key of Object.keys(defaults)) {
78
+ merged[key] = mergeWithDefaults(defaults[key], existing[key]);
79
+ }
80
+ return merged;
81
+ }
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 - A render prop that receives the current value for rendering if visible.
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 Conditional<State extends ReadOnlyAtomLike<unknown>>({ state, on, children }: {
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 { useCallback } from 'react';
2
- export { Render, RenderWithUpdate, Conditional };
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 - A render prop that receives the current value for rendering if visible.
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 Conditional({ state, on, children }) {
69
+ function ConditionalRender({ state, on, children }) {
51
70
  const value = state.use();
52
- const show = on(value); // on should not be expensive, memorizing just adds overhead
71
+ const show = on(value);
53
72
  if (!show) {
54
73
  return null;
55
74
  }
package/package.json CHANGED
@@ -1,53 +1,63 @@
1
1
  {
2
- "name": "juststore",
3
- "version": "1.0.1",
4
- "description": "A small, expressive, and type-safe state management library for React.",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "author": "Yusing",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/yusing/juststore.git"
12
- },
13
- "homepage": "https://github.com/yusing/juststore",
14
- "bugs": {
15
- "url": "https://github.com/yusing/juststore/issues"
16
- },
17
- "exports": {
18
- ".": {
19
- "types": "./dist/index.d.ts",
20
- "import": "./dist/index.js"
21
- }
22
- },
23
- "sideEffects": false,
24
- "files": ["dist"],
25
- "license": "AGPL-3.0-only",
26
- "keywords": ["state", "react", "typescript", "hooks", "store", "state management", "react hooks"],
27
- "scripts": {
28
- "build": "bun --bun tsc",
29
- "typecheck": "bun --bun tsc --noEmit",
30
- "lint": "bun --bun biome check",
31
- "format": "bun --bun biome format --write",
32
- "format:check": "bun --bun biome format",
33
- "prepublishOnly": "bun run build",
34
- "publish": "npm publish --access public",
35
- "prepare": "husky"
36
- },
37
- "peerDependencies": {
38
- "react": ">=18.0.0"
39
- },
40
- "dependencies": {
41
- "change-case": "^5.4.4",
42
- "react-fast-compare": "^3.2.2"
43
- },
44
- "devDependencies": {
45
- "@biomejs/biome": "^2.4.2",
46
- "@eslint/js": "^10.0.1",
47
- "@types/node": "^25.3.0",
48
- "@types/react": "^19.2.14",
49
- "husky": "^9.1.7",
50
- "react": "^19.2.4",
51
- "typescript": "^5.9.3"
52
- }
2
+ "name": "juststore",
3
+ "version": "1.1.1",
4
+ "description": "A small, expressive, and type-safe state management library for React.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "author": "Yusing",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/yusing/juststore.git"
12
+ },
13
+ "homepage": "https://github.com/yusing/juststore",
14
+ "bugs": {
15
+ "url": "https://github.com/yusing/juststore/issues"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "sideEffects": false,
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "license": "AGPL-3.0-only",
28
+ "keywords": [
29
+ "state",
30
+ "react",
31
+ "typescript",
32
+ "hooks",
33
+ "store",
34
+ "state management",
35
+ "react hooks"
36
+ ],
37
+ "scripts": {
38
+ "build": "bun --bun tsc",
39
+ "typecheck": "bun --bun tsc --noEmit",
40
+ "lint": "biome check",
41
+ "format": "biome format --write",
42
+ "format:check": "biome format",
43
+ "prepublishOnly": "bun run build",
44
+ "publish": "npm publish --access public",
45
+ "prepare": "husky"
46
+ },
47
+ "peerDependencies": {
48
+ "react": ">=18.0.0"
49
+ },
50
+ "dependencies": {
51
+ "change-case": "^5.4.4",
52
+ "react-fast-compare": "^3.2.2"
53
+ },
54
+ "devDependencies": {
55
+ "@biomejs/biome": "^2.4.6",
56
+ "@eslint/js": "^10.0.1",
57
+ "@types/node": "^25.4.0",
58
+ "@types/react": "^19.2.14",
59
+ "husky": "^9.1.7",
60
+ "react": "^19.2.4",
61
+ "typescript": "^5.9.3"
62
+ }
53
63
  }