valtio-define 0.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-PRESENT Hairyf <wwu710632@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,253 @@
1
+ # valtio-define
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![bundle][bundle-src]][bundle-href]
6
+ [![JSDocs][jsdocs-src]][jsdocs-href]
7
+ [![License][license-src]][license-href]
8
+
9
+ ⚡ Quickly create a fully functional and robust Valtio factory
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install valtio-define
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Basic Store
20
+
21
+ ```tsx
22
+ import { defineStore, useStore } from 'valtio-define'
23
+
24
+ const store = defineStore({
25
+ state: () => ({ count: 0 }),
26
+ actions: {
27
+ increment() {
28
+ this.count++
29
+ },
30
+ },
31
+ })
32
+
33
+ function Counter() {
34
+ const state = useStore(store)
35
+ return (
36
+ <div>
37
+ <button onClick={store.increment}>Increment</button>
38
+ <div>{state.count}</div>
39
+ </div>
40
+ )
41
+ }
42
+ ```
43
+
44
+ ### With Getters
45
+
46
+ ```tsx
47
+ const store = defineStore({
48
+ state: () => ({ count: 0 }),
49
+ getters: {
50
+ doubled() {
51
+ return this.count * 2
52
+ },
53
+ },
54
+ actions: {
55
+ increment() {
56
+ this.count++
57
+ },
58
+ },
59
+ })
60
+
61
+ function Counter() {
62
+ const state = useStore(store)
63
+ return (
64
+ <div>
65
+ <div>
66
+ Count:
67
+ {state.count}
68
+ </div>
69
+ <div>
70
+ Doubled:
71
+ {state.doubled}
72
+ </div>
73
+ <button onClick={store.increment}>Increment</button>
74
+ </div>
75
+ )
76
+ }
77
+ ```
78
+
79
+ ### Async Actions with Status
80
+
81
+ ```tsx
82
+ import { defineStore, useStatus, useStore } from 'valtio-define'
83
+
84
+ const store = defineStore({
85
+ state: () => ({ data: null }),
86
+ actions: {
87
+ async fetchData() {
88
+ const response = await fetch('/api/data')
89
+ this.data = await response.json()
90
+ },
91
+ },
92
+ })
93
+
94
+ function DataComponent() {
95
+ const state = useStore(store)
96
+ const status = useStatus(store)
97
+
98
+ return (
99
+ <div>
100
+ {status.loading && <div> Store all actions are loading...</div>}
101
+ {status.finished && <div> Store all actions are finished...</div>}
102
+ {status.error && <div> Store all actions are error...</div>}
103
+
104
+ {status.fetchData.finished && <div> Data fetched successfully...</div>}
105
+ {status.fetchData.error && (
106
+ <div>
107
+ {' '}
108
+ Error fetching data:
109
+ {status.fetchData.error.message}
110
+ </div>
111
+ )}
112
+ {state.data && <div>{JSON.stringify(state.data)}</div>}
113
+ <button onClick={store.fetchData}>Fetch Data</button>
114
+ </div>
115
+ )
116
+ }
117
+ ```
118
+
119
+ ### Persistence
120
+
121
+ ```tsx
122
+ const store = defineStore(
123
+ {
124
+ state: () => ({ count: 0 }),
125
+ actions: {
126
+ increment() {
127
+ this.count++
128
+ },
129
+ },
130
+ },
131
+ {
132
+ persist: true // or { key: 'my-store', storage: localStorage, paths: ['count'] }
133
+ }
134
+ )
135
+ ```
136
+
137
+ If the persist is a boolean value, it will use `structure-id` to generate a unique key for the store.
138
+
139
+ ### Subscribe to Changes
140
+
141
+ ```tsx
142
+ const store = defineStore({
143
+ state: () => ({ count: 0 }),
144
+ actions: {
145
+ increment() {
146
+ this.count++
147
+ },
148
+ },
149
+ })
150
+
151
+ // Subscribe to state changes
152
+ const unsubscribe = store.$subscribe((state) => {
153
+ console.log('State changed:', state)
154
+ })
155
+
156
+ // Subscribe to status changes
157
+ const unsubscribeStatus = store.$subscribe.status((status) => {
158
+ console.log('Status changed:', status)
159
+ })
160
+ ```
161
+
162
+ ### Patch State
163
+
164
+ ```tsx
165
+ // Patch with object
166
+ store.$patch({ count: 10 })
167
+
168
+ // Patch with function
169
+ store.$patch((state) => {
170
+ state.count += 5
171
+ })
172
+ ```
173
+
174
+ ### Signal (JSX Component)
175
+
176
+ ```tsx
177
+ function App() {
178
+ return (
179
+ <div>
180
+ {store.$signal(state => (
181
+ <div>
182
+ Count:
183
+ {state.count}
184
+ </div>
185
+ ))}
186
+ {store.$signal.status(status => (
187
+ status.loading && <div>Loading...</div>
188
+ ))}
189
+ </div>
190
+ )
191
+ }
192
+ ```
193
+
194
+ ## API
195
+
196
+ ### `defineStore(store, options?)`
197
+
198
+ Creates a store with state, actions, and getters.
199
+
200
+ **Parameters:**
201
+ - `store.state`: Initial state object or factory function
202
+ - `store.actions`: Object containing action methods
203
+ - `store.getters`: Object containing getter methods
204
+ - `options.persist`: Persistence configuration (boolean or object)
205
+
206
+ **Returns:** Store instance with reactive state and actions
207
+
208
+ ### `useStore(store)`
209
+
210
+ React hook that returns a snapshot of the store state.
211
+
212
+ **Parameters:**
213
+ - `store`: Store instance created by `defineStore`
214
+
215
+ **Returns:** Snapshot of the store state
216
+
217
+ ### `useStatus(store)`
218
+
219
+ React hook that returns the status of all actions.
220
+
221
+ **Parameters:**
222
+ - `store`: Store instance created by `defineStore`
223
+
224
+ **Returns:** Status object with loading, finished, and error states
225
+
226
+ ### `proxyWithPersistent(initialObject, options?)`
227
+
228
+ Creates a persistent proxy state.
229
+
230
+ **Parameters:**
231
+ - `initialObject`: Initial state object
232
+ - `options.key`: Storage key (auto-generated if not provided)
233
+ - `options.storage`: Storage instance (defaults to localStorage)
234
+ - `options.paths`: Array of paths to persist (defaults to all)
235
+
236
+ **Returns:** Persistent proxy state
237
+
238
+ ## License
239
+
240
+ [MIT](./LICENSE) License © [Hairyf](https://github.com/hairyf)
241
+
242
+ <!-- Badges -->
243
+
244
+ [npm-version-src]: https://img.shields.io/npm/v/valtio-define?style=flat&colorA=080f12&colorB=1fa669
245
+ [npm-version-href]: https://npmjs.com/package/valtio-define
246
+ [npm-downloads-src]: https://img.shields.io/npm/dm/valtio-define?style=flat&colorA=080f12&colorB=1fa669
247
+ [npm-downloads-href]: https://npmjs.com/package/valtio-define
248
+ [bundle-src]: https://img.shields.io/bundlephobia/minzip/valtio-define?style=flat&colorA=080f12&colorB=1fa669&label=minzip
249
+ [bundle-href]: https://bundlephobia.com/result?p=valtio-define
250
+ [license-src]: https://img.shields.io/github/license/hairyf/valtio-define.svg?style=flat&colorA=080f12&colorB=1fa669
251
+ [license-href]: https://github.com/hairyf/valtio-define/blob/main/LICENSE
252
+ [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
253
+ [jsdocs-href]: https://www.jsdocs.io/package/valtio-define
@@ -0,0 +1,88 @@
1
+ import { Snapshot } from "valtio";
2
+
3
+ //#region src/types/index.d.ts
4
+ type Actions<S> = Record<string, (this: S, ...args: any) => any>;
5
+ type Getters<S> = Record<string, (this: S) => any>;
6
+ type ActionsOmitThisParameter<A extends Actions<any>> = { [K in keyof A]: (...args: Parameters<A[K]>) => ReturnType<A[K]> };
7
+ interface Status {
8
+ finished: boolean;
9
+ loading: boolean;
10
+ error: Error | null;
11
+ }
12
+ type ActionsStatus<A extends Actions<any>> = Status & { [K in keyof A]: Status };
13
+ type GettersReturnType<G extends Getters<any>> = { [K in keyof G]: ReturnType<G[K]> };
14
+ interface StoreDefine<S extends object, A extends Actions<S>, G extends Getters<S>> {
15
+ state: (() => S) | S;
16
+ actions?: A;
17
+ getters?: G;
18
+ }
19
+ interface StoreOptions {
20
+ persist?: boolean | PersistentOptions;
21
+ }
22
+ interface StoreSignal<S, A extends Actions<S>, G extends Getters<S>> {
23
+ <T>(fn: (state: S & GettersReturnType<G>) => T): T;
24
+ status: <T>(fn: (status: ActionsStatus<A>) => T) => T;
25
+ }
26
+ interface StoreSubscribe<S, A extends Actions<S>, G extends Getters<S>> {
27
+ (listener: (state: S & GettersReturnType<G>) => void): () => void;
28
+ status: (listener: (status: ActionsStatus<A>) => void) => () => void;
29
+ }
30
+ interface StorePatch<S, G extends Getters<S>> {
31
+ (patch: Partial<S> | ((state: S & GettersReturnType<G>) => void)): void;
32
+ }
33
+ type Store<S, A extends Actions<S>, G extends Getters<S>> = {
34
+ $subscribe: StoreSubscribe<S, A, G>;
35
+ $patch: StorePatch<S, G>;
36
+ $state: S & GettersReturnType<G> & ActionsOmitThisParameter<A>;
37
+ $actions: ActionsOmitThisParameter<A>;
38
+ $getters: GettersReturnType<G>;
39
+ $status: ActionsStatus<A>;
40
+ $signal: StoreSignal<S, A, G>;
41
+ } & ActionsOmitThisParameter<A>;
42
+ interface PersistentOptions {
43
+ key?: string;
44
+ storage?: Storage;
45
+ paths?: string[];
46
+ }
47
+ //#endregion
48
+ //#region src/define-store.d.ts
49
+ /**
50
+ * @description Define a store
51
+ * @example
52
+ * ```tsx
53
+ * const store = defineStore({
54
+ * state: () => ({ count: 0 }),
55
+ * actions: {
56
+ * increment() {
57
+ * this.count++
58
+ * },
59
+ * },
60
+ * })
61
+ *
62
+ * store.increment()
63
+ * console.log(store.$state.count) // 1
64
+ *
65
+ * function Component() {
66
+ * const store = useStore(store)
67
+ * return (
68
+ * <div>
69
+ * <button onClick={store.increment}>Increment</button>
70
+ * <div>{store.count}</div>
71
+ * </div>
72
+ * )
73
+ * }
74
+ *
75
+ * ```
76
+ */
77
+ declare function defineStore<S extends object, A extends Actions<S>, G extends Getters<S>>(store: StoreDefine<S, A, G>, options?: StoreOptions): Store<S, A, G>;
78
+ //#endregion
79
+ //#region src/persistent.d.ts
80
+ declare function proxyWithPersistent<T extends object>(initialObject: T, options?: PersistentOptions): T;
81
+ //#endregion
82
+ //#region src/use-status.d.ts
83
+ declare function useStatus<S extends object, A extends Actions<S>, G extends Getters<S>>(store: Store<S, A, G>): Snapshot<ActionsStatus<A>>;
84
+ //#endregion
85
+ //#region src/use-store.d.ts
86
+ declare function useStore<S extends object, A extends Actions<S>, G extends Getters<S>>(store: Store<S, A, G>): Snapshot<S & GettersReturnType<G>>;
87
+ //#endregion
88
+ export { defineStore, proxyWithPersistent, useStatus, useStore };
package/dist/index.mjs ADDED
@@ -0,0 +1,183 @@
1
+ import { createElement } from "react";
2
+ import { proxy, subscribe, useSnapshot } from "valtio";
3
+ import { jsonTryParse } from "@hairy/utils";
4
+ import { generateStructureId } from "structure-id";
5
+
6
+ //#region src/utils/index.ts
7
+ function track(action, status) {
8
+ let loadings = 0;
9
+ const tracking = () => {
10
+ loadings++ === 0 && (status.loading = true);
11
+ };
12
+ const done = () => {
13
+ !--loadings && (status.loading = false);
14
+ };
15
+ const fulfilled = (value) => {
16
+ status.finished = true;
17
+ done();
18
+ return value;
19
+ };
20
+ const rejected = (error) => {
21
+ status.error = error;
22
+ done();
23
+ throw error;
24
+ };
25
+ return function(...args) {
26
+ tracking();
27
+ try {
28
+ const value = action(...args);
29
+ return value instanceof Promise ? value.then(fulfilled, rejected) : fulfilled(value);
30
+ } catch (error) {
31
+ rejected(error);
32
+ }
33
+ };
34
+ }
35
+ function get(obj, path) {
36
+ return path.split(".").reduce((result, key) => result?.[key], obj);
37
+ }
38
+ function set(obj, path, value) {
39
+ const keys = path.split(".");
40
+ const lastKey = keys.pop();
41
+ const target = keys.reduce((result, key) => result[key] ??= {}, obj);
42
+ target[lastKey] = value;
43
+ }
44
+
45
+ //#endregion
46
+ //#region src/persistent.ts
47
+ function proxyWithPersistent(initialObject, options = {}) {
48
+ options.key = options.key || generateStructureId(initialObject);
49
+ const storage = options.storage || (typeof localStorage !== "undefined" ? localStorage : void 0);
50
+ const state = proxy(jsonTryParse(storage?.getItem(options.key)) || initialObject);
51
+ subscribe(state, () => {
52
+ const paths = options.paths || Object.keys(state);
53
+ const statePaths = {};
54
+ for (const key of paths) set(statePaths, key, get(state, key));
55
+ storage?.setItem(options.key, JSON.stringify(statePaths));
56
+ });
57
+ return state;
58
+ }
59
+
60
+ //#endregion
61
+ //#region src/define-store.ts
62
+ /**
63
+ * @description Define a store
64
+ * @example
65
+ * ```tsx
66
+ * const store = defineStore({
67
+ * state: () => ({ count: 0 }),
68
+ * actions: {
69
+ * increment() {
70
+ * this.count++
71
+ * },
72
+ * },
73
+ * })
74
+ *
75
+ * store.increment()
76
+ * console.log(store.$state.count) // 1
77
+ *
78
+ * function Component() {
79
+ * const store = useStore(store)
80
+ * return (
81
+ * <div>
82
+ * <button onClick={store.increment}>Increment</button>
83
+ * <div>{store.count}</div>
84
+ * </div>
85
+ * )
86
+ * }
87
+ *
88
+ * ```
89
+ */
90
+ function defineStore(store, options = {}) {
91
+ const state = typeof store.state === "function" ? store.state() : store.state;
92
+ const getters = store.getters || {};
93
+ const actions = store.actions || {};
94
+ const status = {};
95
+ status.finished = false;
96
+ status.loading = false;
97
+ status.error = null;
98
+ const $status = proxy(status);
99
+ const $state = options.persist ? proxyWithPersistent(state, options.persist === true ? {} : options.persist) : proxy(state);
100
+ const $actions = {};
101
+ const $getters = {};
102
+ setupActions($state, actions, $actions, $status);
103
+ setupGetters(state, $state, getters, $getters);
104
+ setupStatus($actions, $status);
105
+ function $subscribe(listener) {
106
+ return subscribe($state, () => listener($state));
107
+ }
108
+ $subscribe.status = function(listener) {
109
+ return subscribe($status, () => listener($status));
110
+ };
111
+ function $patch(patch) {
112
+ if (typeof patch === "function") patch($state);
113
+ else Object.assign($state, patch);
114
+ }
115
+ function $signal(fn) {
116
+ return createElement(() => fn(useSnapshot($state)));
117
+ }
118
+ $signal.status = function(fn) {
119
+ return createElement(() => fn(useSnapshot($status)));
120
+ };
121
+ return {
122
+ $subscribe,
123
+ $patch,
124
+ $state,
125
+ $status,
126
+ $actions,
127
+ $getters,
128
+ $signal,
129
+ ...$actions
130
+ };
131
+ }
132
+ function setupActions($state, actions, $actions, $status) {
133
+ for (const key in actions) {
134
+ $status[key] = {
135
+ finished: false,
136
+ loading: false,
137
+ error: null
138
+ };
139
+ $actions[key] = track(actions[key].bind($state), $status[key]);
140
+ Object.defineProperty($state, key, { get: () => $actions[key] });
141
+ }
142
+ }
143
+ function setupGetters(state, $state, getters, $getters) {
144
+ for (const key in getters) {
145
+ Object.defineProperty($getters, key, {
146
+ get: () => state[key],
147
+ enumerable: true
148
+ });
149
+ Object.defineProperty(state, key, {
150
+ get: () => getters[key].call($state),
151
+ enumerable: true
152
+ });
153
+ }
154
+ }
155
+ function setupStatus($actions, $status) {
156
+ Object.defineProperty($status, "loading", {
157
+ get: () => Object.keys($actions).some((key) => $status[key].loading),
158
+ enumerable: true
159
+ });
160
+ Object.defineProperty($status, "finished", {
161
+ get: () => Object.keys($actions).every((key) => $status[key].finished),
162
+ enumerable: true
163
+ });
164
+ Object.defineProperty($status, "error", {
165
+ get: () => Object.keys($actions).find((key) => $status[key].error),
166
+ enumerable: true
167
+ });
168
+ }
169
+
170
+ //#endregion
171
+ //#region src/use-status.ts
172
+ function useStatus(store) {
173
+ return useSnapshot(store.$status);
174
+ }
175
+
176
+ //#endregion
177
+ //#region src/use-store.ts
178
+ function useStore(store) {
179
+ return useSnapshot(store.$state);
180
+ }
181
+
182
+ //#endregion
183
+ export { defineStore, proxyWithPersistent, useStatus, useStore };
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "valtio-define",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "⚡quickly create a fully functional and robust Valtio factory",
6
+ "author": "Hairyf <wwu710632@gmail.com>",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/hairyf/valtio-define#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/hairyf/valtio-define.git"
12
+ },
13
+ "bugs": "https://github.com/hairyf/valtio-define/issues",
14
+ "keywords": [],
15
+ "sideEffects": false,
16
+ "exports": {
17
+ ".": "./dist/index.mjs",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "main": "./dist/index.mjs",
21
+ "module": "./dist/index.mjs",
22
+ "types": "./dist/index.d.mts",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "peerDependencies": {
27
+ "react": "^18.2.0"
28
+ },
29
+ "dependencies": {
30
+ "@hairy/utils": "^1.39.0",
31
+ "structure-id": "^1.2.9",
32
+ "valtio": "^2.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@antfu/eslint-config": "^6.2.0",
36
+ "@antfu/ni": "^27.0.1",
37
+ "@antfu/utils": "^9.3.0",
38
+ "@types/node": "^24.10.0",
39
+ "@types/react": "^19.2.6",
40
+ "@vitejs/plugin-react": "^5.1.1",
41
+ "bumpp": "^10.3.1",
42
+ "eslint": "^9.39.1",
43
+ "lint-staged": "^16.2.6",
44
+ "react": "^19.2.0",
45
+ "simple-git-hooks": "^2.13.1",
46
+ "tinyexec": "^1.0.2",
47
+ "tsdown": "^0.16.0",
48
+ "tsx": "^4.20.6",
49
+ "typescript": "^5.9.3",
50
+ "vite": "^7.2.1",
51
+ "vitest": "^4.0.7",
52
+ "vitest-package-exports": "^0.1.1",
53
+ "yaml": "^2.8.1"
54
+ },
55
+ "simple-git-hooks": {
56
+ "pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged"
57
+ },
58
+ "lint-staged": {
59
+ "*": "eslint --fix"
60
+ },
61
+ "scripts": {
62
+ "build": "tsdown",
63
+ "dev": "tsdown --watch",
64
+ "lint": "eslint",
65
+ "release": "bumpp",
66
+ "start": "tsx src/index.ts",
67
+ "test": "vitest",
68
+ "typecheck": "tsc"
69
+ }
70
+ }