juststore 1.1.1 → 1.2.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.
@@ -0,0 +1,42 @@
1
+ import type { FieldValues } from "./path";
2
+ import { type StoreOptions } from "./root";
3
+ import type { State, StoreRoot } from "./types";
4
+ export { createStore, type Store };
5
+ /**
6
+ * A persistent, hierarchical, cross-tab synchronized key-value store with React bindings.
7
+ *
8
+ * - Dot-path addressing for nested values (e.g. "config.ui.theme").
9
+ * - Immutable partial updates with automatic object/array creation.
10
+ * - Persists root namespaces to localStorage with an in-memory mirror (no localStorage on SSR).
11
+ * - Cross-tab synchronization via BroadcastChannel (no-ops on SSR).
12
+ * - Fine-grained subscriptions built on useSyncExternalStore.
13
+ * - Type-safe paths using FieldPath.
14
+ * - Dynamic deep access via Proxy for ergonomic usage like `store.a.b.c.use()` and `store.a.b.c.set(v)`.
15
+ */
16
+ type Store<T extends FieldValues> = StoreRoot<T> & {
17
+ [K in keyof T]-?: State<T[K]>;
18
+ };
19
+ /**
20
+ * Creates a persistent, hierarchical store with localStorage backing and cross-tab synchronization.
21
+ *
22
+ * @param namespace - Unique identifier for the store, used as the localStorage key prefix
23
+ * @param defaultValue - Initial state shape; merged with any existing persisted data
24
+ * @param options - Configuration options
25
+ * @param options.memoryOnly - When true, disables localStorage persistence (default: false)
26
+ * @returns A proxy object providing both path-based and dynamic property access to the store
27
+ *
28
+ * @example
29
+ * const store = createStore('app', {
30
+ * user: { name: 'Guest' },
31
+ * todos: []
32
+ * })
33
+ *
34
+ * // Dynamic access
35
+ * store.user.name.use()
36
+ * store.todos.push({ text: 'New todo' })
37
+ *
38
+ * // Path-based access
39
+ * store.use('user.name')
40
+ * store.set('user.name', 'Alice')
41
+ */
42
+ declare function createStore<T extends FieldValues>(namespace: string, defaultValue: T, options?: StoreOptions): Store<T>;
@@ -0,0 +1,40 @@
1
+ import { createRootNode } from "./node";
2
+ import { createStoreRoot } from "./root";
3
+ export { createStore };
4
+ /**
5
+ * Creates a persistent, hierarchical store with localStorage backing and cross-tab synchronization.
6
+ *
7
+ * @param namespace - Unique identifier for the store, used as the localStorage key prefix
8
+ * @param defaultValue - Initial state shape; merged with any existing persisted data
9
+ * @param options - Configuration options
10
+ * @param options.memoryOnly - When true, disables localStorage persistence (default: false)
11
+ * @returns A proxy object providing both path-based and dynamic property access to the store
12
+ *
13
+ * @example
14
+ * const store = createStore('app', {
15
+ * user: { name: 'Guest' },
16
+ * todos: []
17
+ * })
18
+ *
19
+ * // Dynamic access
20
+ * store.user.name.use()
21
+ * store.todos.push({ text: 'New todo' })
22
+ *
23
+ * // Path-based access
24
+ * store.use('user.name')
25
+ * store.set('user.name', 'Alice')
26
+ */
27
+ function createStore(namespace, defaultValue, options = {}) {
28
+ const storeApi = createStoreRoot(namespace, defaultValue, options);
29
+ return new Proxy(storeApi, {
30
+ get(target, prop) {
31
+ if (prop in target) {
32
+ return target[prop];
33
+ }
34
+ if (typeof prop === "string" || typeof prop === "number") {
35
+ return createRootNode(target, prop);
36
+ }
37
+ return undefined;
38
+ },
39
+ });
40
+ }
@@ -0,0 +1,143 @@
1
+ import type { FieldPath, FieldPathValue, FieldValues, IsEqual } from "./path";
2
+ export type { AllowedKeys, ArrayProxy, ArrayState, DerivedStateProps, ObjectMutationMethods, ObjectState, Prettify, ReadOnlyState, State, StoreRoot, StoreSetStateValue, StoreUseComputeFn, StoreUseState, ValueState, };
3
+ type Prettify<T> = {
4
+ [K in keyof T]: T[K];
5
+ } & {};
6
+ type AllowedKeys<T> = Exclude<keyof T, keyof ValueState<unknown> | keyof ObjectMutationMethods>;
7
+ type ArrayMutationMethods<T> = Prettify<Pick<Array<T>, "push" | "pop" | "shift" | "unshift" | "splice" | "reverse" | "sort" | "fill" | "copyWithin">>;
8
+ /** Type for array proxy with index access */
9
+ type ArrayProxy<T, ElementState = State<T>> = ArrayMutationMethods<NonNullable<T>> & {
10
+ /** Read without subscribing. Returns array or undefined for missing paths. */
11
+ readonly value: T[];
12
+ /**
13
+ * Length of the underlying array. Runtime may return undefined when the
14
+ * current value is not an array at the path. Prefer `Array.isArray(x) && x.length` when unsure.
15
+ */
16
+ readonly length: number;
17
+ /** Subscribe to array length changes without subscribing to individual element changes. */
18
+ useLength(): number;
19
+ /** Numeric index access never returns undefined at the type level because
20
+ * the proxy always returns another proxy object, even if the underlying value doesn't exist.
21
+ */
22
+ [K: number]: ElementState;
23
+ /** Safe accessor that never returns undefined at the type level */
24
+ at(index: number): ElementState;
25
+ /** Insert items into the array in sorted order using the provided comparison function. */
26
+ sortedInsert(cmp: (a: T, b: T) => number, ...items: T[]): number;
27
+ };
28
+ type ObjectProxy<T extends FieldValues> = {
29
+ /** Virtual state for the object's keys.
30
+ *
31
+ * This does NOT read from a real `keys` property on the stored object; it results in a stable array of keys.
32
+ */
33
+ readonly keys: ReadOnlyState<FieldPath<T>[]>;
34
+ } & {
35
+ [K in keyof T]-?: State<T[K]>;
36
+ };
37
+ type ObjectMutationMethods = {
38
+ /** Rename a key in an object. */
39
+ rename: (oldKey: string, newKey: string) => void;
40
+ };
41
+ /** Tuple returned by Store.use(path). */
42
+ type StoreUseState<T> = Readonly<[
43
+ T,
44
+ (value: StoreSetStateValue<T>, skipUpdate?: boolean) => void
45
+ ]>;
46
+ /** Value type for set method. */
47
+ type StoreSetStateValue<T> = T | ((prev: T) => T);
48
+ /** Public API returned by createStore(namespace, defaultValue). */
49
+ type StoreRoot<T extends FieldValues> = {
50
+ /** Get the state object for a path. */
51
+ state: <P extends FieldPath<T>>(path: P) => State<FieldPathValue<T, P>>;
52
+ /** Subscribe and read the value at path. Re-renders when the value changes. */
53
+ use: <P extends FieldPath<T>>(path: P) => FieldPathValue<T, P>;
54
+ /** Subscribe and read the debounced value at path. Re-renders when the value changes. */
55
+ useDebounce: <P extends FieldPath<T>>(path: P, delay: number) => FieldPathValue<T, P>;
56
+ /** Convenience hook returning [value, setValue] for the path. */
57
+ useState: <P extends FieldPath<T>>(path: P) => StoreUseState<FieldPathValue<T, P>>;
58
+ /** Read without subscribing. */
59
+ value: <P extends FieldPath<T>>(path: P) => FieldPathValue<T, P>;
60
+ /** Set value at path (creates intermediate nodes as needed). */
61
+ set: <P extends FieldPath<T>>(path: P, value: StoreSetStateValue<FieldPathValue<T, P>>, skipUpdate?: boolean) => void;
62
+ /** Delete value at path (for arrays, removes index; for objects, deletes key). */
63
+ reset: <P extends FieldPath<T>>(path: P) => void;
64
+ /** Rename a key in an object. */
65
+ rename: <P extends FieldPath<T>>(path: P, oldKey: string, newKey: string) => void;
66
+ /** Subscribe to changes at path and invoke listener with the new value
67
+ *
68
+ * @returns A function to unsubscribe from the path.
69
+ */
70
+ subscribe: <P extends FieldPath<T>>(path: P, listener: (value: FieldPathValue<T, P>) => void) => () => void;
71
+ /** Compute a derived value from the current value, similar to useState + useMemo */
72
+ useCompute: <P extends FieldPath<T>, R>(path: P, fn: StoreUseComputeFn<T, P, R>, deps?: readonly unknown[]) => R;
73
+ /** Notify listeners at path. */
74
+ notify: <P extends FieldPath<T>>(path: P) => void;
75
+ };
76
+ /** Common methods available on any deep proxy node */
77
+ type ValueState<T> = {
78
+ /** Read without subscribing. */
79
+ readonly value: T;
80
+ /** The field name for the proxy. */
81
+ readonly field: string;
82
+ /** The path for the proxy. */
83
+ readonly path: string;
84
+ /** Subscribe and read the value at path. Re-renders when the value changes. */
85
+ use(): T;
86
+ /** Subscribe and read the debounced value at path. Re-renders when the value changes. */
87
+ useDebounce(delay: number): T;
88
+ /** Convenience hook returning [value, setValue] for the path. */
89
+ useState(): StoreUseState<T>;
90
+ /** Set value at path (creates intermediate nodes as needed). */
91
+ set(value: T | ((prev: T) => T), skipUpdate?: boolean): void;
92
+ /** Delete value at path (for arrays, removes index; for objects, deletes key). */
93
+ reset(): void;
94
+ /** Subscribe to changes at path and invoke listener with the new value.
95
+ *
96
+ * @returns A function to unsubscribe.
97
+ * @example
98
+ *
99
+ * useEffect(() => {
100
+ * const unsubscribe = store.a.b.c.subscribe(value => {
101
+ * console.log(value)
102
+ * })
103
+ * return unsubscribe
104
+ * }, [])
105
+ */
106
+ subscribe(listener: (value: T) => void): () => void;
107
+ /** Compute a derived value from the current value, similar to useState + useMemo */
108
+ useCompute: <R>(fn: (value: T) => R, deps?: readonly unknown[]) => R;
109
+ /** Ensure the value is an array. */
110
+ ensureArray(): NonNullable<T> extends (infer U)[] ? ArrayState<U> : never;
111
+ /** Ensure the value is an object. */
112
+ ensureObject(): NonNullable<T> extends FieldValues ? ObjectState<NonNullable<T>> : never;
113
+ /** Return a new state with a default value, and make the type non-nullable */
114
+ withDefault(defaultValue: T): State<NonNullable<T>>;
115
+ /** Virtual state derived from the current value.
116
+ *
117
+ * @returns ArrayState if the derived value is an array, ObjectState if the derived value is an object, otherwise State.
118
+ * @example
119
+ * const state = store.a.b.c.derived({
120
+ * from: value => value + 1,
121
+ * to: value => value - 1
122
+ * })
123
+ * state.use() // returns the derived value
124
+ * state.set(10) // sets the derived value
125
+ * state.reset() // resets the derived value
126
+ */
127
+ derived: <R>({ from, to }: DerivedStateProps<T, R>) => State<R>;
128
+ /** Notify listener of current value. */
129
+ notify(): void;
130
+ };
131
+ /**
132
+ * A read-only state that provides access to the value, use, Render, and Show methods.
133
+ */
134
+ type ReadOnlyState<T> = Prettify<Pick<ValueState<Readonly<Required<T>>>, "value" | "use" | "useCompute">>;
135
+ type State<T> = IsEqual<T, unknown> extends true ? never : [NonNullable<T>] extends [readonly (infer U)[]] ? ArrayState<U, T> : [NonNullable<T>] extends [FieldValues] ? ObjectState<NonNullable<T>, T> : ValueState<T>;
136
+ type ArrayState<TValue, TArray = TValue[]> = IsEqual<TValue, unknown> extends true ? never : ValueState<TArray> & ArrayProxy<TValue>;
137
+ type ObjectState<TNonNullable extends FieldValues, TObject = TNonNullable> = ObjectProxy<TNonNullable> & ValueState<TObject> & ObjectMutationMethods;
138
+ /** Type for useCompute function. */
139
+ type StoreUseComputeFn<T extends FieldValues, P extends FieldPath<T>, R> = (value: FieldPathValue<T, P>) => R;
140
+ type DerivedStateProps<T, R> = {
141
+ from?: (value: T) => R;
142
+ to?: (value: R) => T;
143
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import type { Atom } from "./atom";
2
+ import type { StoreSetStateValue, ValueState } from "./types";
3
+ export { Conditional, ConditionalRender, Render, RenderWithUpdate };
4
+ type AtomLike<T> = Pick<Atom<T> | ValueState<T>, "use" | "set" | "value">;
5
+ type ReadOnlyAtomLike<T> = Pick<Atom<T> | ValueState<T>, "use" | "useCompute" | "value">;
6
+ type RenderProps<State extends ReadOnlyAtomLike<unknown>> = {
7
+ state: State;
8
+ children: (value: State["value"]) => React.ReactNode;
9
+ };
10
+ type RenderWithUpdateProps<State extends AtomLike<unknown>> = {
11
+ state: State;
12
+ children: (value: State["value"], update: (value: StoreSetStateValue<State["value"]>) => void) => React.ReactNode;
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
+ };
24
+ /**
25
+ * Renders the provided children function with the current value from the state.
26
+ *
27
+ * @template T The type of the state value.
28
+ * @param props - The props object.
29
+ * @param props.state - The ValueState whose value will be passed to children.
30
+ * @param props.children - A render prop that receives the current value.
31
+ * @returns The result of calling children with the current value.
32
+ */
33
+ declare function Render<State extends ReadOnlyAtomLike<unknown>>({ state, children, }: RenderProps<State>): import("react").ReactNode;
34
+ /**
35
+ * Renders the provided children function with the current value and an update function.
36
+ *
37
+ * The update function can set the value directly or with an updater function.
38
+ *
39
+ * @template T The type of the state value.
40
+ * @template U The type allowed for updating the state (value or updater).
41
+ * @param props - The props object.
42
+ * @param props.state - The ValueState whose value will be passed to children.
43
+ * @param props.children - A render prop that receives the current value and update function.
44
+ * @returns The result of calling children with the current value and update function.
45
+ */
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;
60
+ /**
61
+ * Conditionally renders the children function based on the result of the `on` predicate.
62
+ *
63
+ * It returns null if the predicate returns false.
64
+ *
65
+ * @template T The type of the state value.
66
+ * @param props - The props object.
67
+ * @param props.state - The ValueState whose value will be used.
68
+ * @param props.on - A predicate that receives the value and returns whether to show children.
69
+ * @param props.children - The render function that receives the value.
70
+ * @returns The result of children if the predicate returns true, otherwise null.
71
+ */
72
+ declare function ConditionalRender<State extends ReadOnlyAtomLike<unknown>>({ state, on, children, }: ConditionalRenderProps<State>): import("react").ReactNode;
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Activity, useCallback } from "react";
3
+ export { Conditional, ConditionalRender, Render, RenderWithUpdate };
4
+ /**
5
+ * Renders the provided children function with the current value from the state.
6
+ *
7
+ * @template T The type of the state value.
8
+ * @param props - The props object.
9
+ * @param props.state - The ValueState whose value will be passed to children.
10
+ * @param props.children - A render prop that receives the current value.
11
+ * @returns The result of calling children with the current value.
12
+ */
13
+ function Render({ state, children, }) {
14
+ const value = state.use();
15
+ return children(value);
16
+ }
17
+ /**
18
+ * Renders the provided children function with the current value and an update function.
19
+ *
20
+ * The update function can set the value directly or with an updater function.
21
+ *
22
+ * @template T The type of the state value.
23
+ * @template U The type allowed for updating the state (value or updater).
24
+ * @param props - The props object.
25
+ * @param props.state - The ValueState whose value will be passed to children.
26
+ * @param props.children - A render prop that receives the current value and update function.
27
+ * @returns The result of calling children with the current value and update function.
28
+ */
29
+ function RenderWithUpdate({ state, children, }) {
30
+ const value = state.use();
31
+ const update = useCallback((value) => {
32
+ if (typeof value !== "function") {
33
+ state.set(value);
34
+ }
35
+ else {
36
+ state.set(value(state.value));
37
+ }
38
+ }, [state]);
39
+ return children(value, update);
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
+ }
57
+ /**
58
+ * Conditionally renders the children function based on the result of the `on` predicate.
59
+ *
60
+ * It returns null if the predicate returns false.
61
+ *
62
+ * @template T The type of the state value.
63
+ * @param props - The props object.
64
+ * @param props.state - The ValueState whose value will be used.
65
+ * @param props.on - A predicate that receives the value and returns whether to show children.
66
+ * @param props.children - The render function that receives the value.
67
+ * @returns The result of children if the predicate returns true, otherwise null.
68
+ */
69
+ function ConditionalRender({ state, on, children, }) {
70
+ const value = state.use();
71
+ const show = on(value);
72
+ if (!show) {
73
+ return null;
74
+ }
75
+ return children(value);
76
+ }
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Atom } from './atom';
2
2
  import type { StoreSetStateValue, ValueState } from './types';
3
- export { Render, RenderWithUpdate, Conditional, ConditionalRender };
3
+ export { Conditional, ConditionalRender, Render, RenderWithUpdate };
4
4
  type AtomLike<T> = Pick<Atom<T> | ValueState<T>, 'use' | 'set' | 'value'>;
5
5
  type ReadOnlyAtomLike<T> = Pick<Atom<T> | ValueState<T>, 'use' | 'useCompute' | 'value'>;
6
6
  type RenderProps<State extends ReadOnlyAtomLike<unknown>> = {
package/dist/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Activity, useCallback } from 'react';
3
- export { Render, RenderWithUpdate, Conditional, ConditionalRender };
3
+ export { Conditional, ConditionalRender, Render, RenderWithUpdate };
4
4
  /**
5
5
  * Renders the provided children function with the current value from the state.
6
6
  *
package/package.json CHANGED
@@ -1,63 +1,63 @@
1
1
  {
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
- }
2
+ "name": "juststore",
3
+ "version": "1.2.0",
4
+ "description": "A small, expressive, and type-safe state management library for React.",
5
+ "keywords": [
6
+ "hooks",
7
+ "react",
8
+ "react hooks",
9
+ "state",
10
+ "state management",
11
+ "store",
12
+ "typescript"
13
+ ],
14
+ "homepage": "https://github.com/yusing/juststore",
15
+ "bugs": {
16
+ "url": "https://github.com/yusing/juststore/issues"
17
+ },
18
+ "license": "AGPL-3.0-only",
19
+ "author": "Yusing",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/yusing/juststore.git"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "build": "bun --bun tsc",
39
+ "format": "oxfmt --write",
40
+ "format:check": "oxfmt",
41
+ "lint": "oxlint",
42
+ "prepare": "husky",
43
+ "prepublishOnly": "bun run build",
44
+ "typecheck": "bun --bun tsc --noEmit"
45
+ },
46
+ "dependencies": {
47
+ "change-case": "^5.4.4",
48
+ "react-fast-compare": "^3.2.2"
49
+ },
50
+ "devDependencies": {
51
+ "@eslint/js": "^10.0.1",
52
+ "@types/node": "^25.5.0",
53
+ "@types/react": "^19.2.14",
54
+ "husky": "^9.1.7",
55
+ "oxfmt": "^0.42.0",
56
+ "oxlint": "^1.57.0",
57
+ "react": "^19.2.4",
58
+ "typescript": "^6.0.2"
59
+ },
60
+ "peerDependencies": {
61
+ "react": ">=18.0.0"
62
+ }
63
63
  }