@uistate/react 1.0.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 ADDED
@@ -0,0 +1,160 @@
1
+ # @uistate/react
2
+
3
+ React adapter for [@uistate/core](https://www.npmjs.com/package/@uistate/core). Five hooks and a provider — that's the entire API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @uistate/react @uistate/core react
9
+ ```
10
+
11
+ **Peer dependencies:** `@uistate/core >=5.0.0` and `react >=18.0.0`.
12
+
13
+ ## Quick Start
14
+
15
+ ```jsx
16
+ import { createEventState } from '@uistate/core';
17
+ import { EventStateProvider, usePath, useIntent } from '@uistate/react';
18
+
19
+ // Store lives outside React
20
+ const store = createEventState({
21
+ state: { count: 0 },
22
+ });
23
+
24
+ // Business logic lives outside React
25
+ store.subscribe('intent.increment', () => {
26
+ store.set('state.count', store.get('state.count') + 1);
27
+ });
28
+
29
+ function Counter() {
30
+ const count = usePath('state.count');
31
+ const increment = useIntent('intent.increment');
32
+ return <button onClick={() => increment(true)}>Count: {count}</button>;
33
+ }
34
+
35
+ function App() {
36
+ return (
37
+ <EventStateProvider store={store}>
38
+ <Counter />
39
+ </EventStateProvider>
40
+ );
41
+ }
42
+ ```
43
+
44
+ ## API
45
+
46
+ ### `<EventStateProvider store={store}>`
47
+
48
+ Makes a store available to all descendant hooks via React Context. The store is created outside React — the provider is pure dependency injection, not a state container.
49
+
50
+ ```jsx
51
+ <EventStateProvider store={store}>
52
+ <App />
53
+ </EventStateProvider>
54
+ ```
55
+
56
+ ### `useStore()`
57
+
58
+ Returns the store from context. Throws if called outside a provider.
59
+
60
+ ```jsx
61
+ const store = useStore();
62
+ ```
63
+
64
+ ### `usePath(path)`
65
+
66
+ Subscribe to a dot-path. Re-renders only when the value at that path changes. Uses `useSyncExternalStore` for concurrent-mode safety.
67
+
68
+ ```jsx
69
+ const tasks = usePath('state.tasks');
70
+ const userName = usePath('state.user.name');
71
+ const filtered = usePath('derived.tasks.filtered');
72
+ ```
73
+
74
+ ### `useIntent(path)`
75
+
76
+ Returns a stable, memoized function that publishes a value to a path. Safe to pass as a prop without causing re-renders.
77
+
78
+ ```jsx
79
+ const addTask = useIntent('intent.addTask');
80
+ const setFilter = useIntent('intent.changeFilter');
81
+
82
+ // In a handler:
83
+ addTask('Buy milk');
84
+ setFilter('active');
85
+ ```
86
+
87
+ ### `useWildcard(path)`
88
+
89
+ Subscribe to a wildcard path. Re-renders when any child of that path changes. Returns the parent object.
90
+
91
+ ```jsx
92
+ const user = useWildcard('state.user.*');
93
+ // Re-renders when state.user.name, state.user.email, etc. change
94
+ ```
95
+
96
+ ### `useAsync(path)`
97
+
98
+ Async data fetching with automatic status tracking. Returns `{ data, status, error, execute, cancel }`.
99
+
100
+ ```jsx
101
+ function UserList() {
102
+ const { data, status, error, execute, cancel } = useAsync('users');
103
+
104
+ useEffect(() => {
105
+ execute((signal) =>
106
+ fetch('/api/users', { signal }).then(r => r.json())
107
+ );
108
+ }, [execute]);
109
+
110
+ if (status === 'loading') return <Spinner />;
111
+ if (error) return <Error message={error} />;
112
+ return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
113
+ }
114
+ ```
115
+
116
+ Calling `execute` again auto-aborts the previous in-flight request. No race conditions.
117
+
118
+ ## Architecture
119
+
120
+ The recommended pattern is three namespaces:
121
+
122
+ | Namespace | Purpose | Hooks |
123
+ |-----------|---------|-------|
124
+ | `state.*` | Authoritative application state | `usePath` |
125
+ | `derived.*` | Computed projections | `usePath` |
126
+ | `intent.*` | Write-only signals from the UI | `useIntent` |
127
+
128
+ This gives you Model-View-Intent (MVI) inside a single store:
129
+
130
+ - **state.\*** is the Model
131
+ - **derived.\*** is the ViewModel
132
+ - **intent.\*** is the Controller
133
+
134
+ ```jsx
135
+ // Component only declares what it reads and what it publishes
136
+ function Filters() {
137
+ const filter = usePath('state.filter'); // read
138
+ const setFilter = useIntent('intent.changeFilter'); // write
139
+ return <button onClick={() => setFilter('active')}>{filter}</button>;
140
+ }
141
+ ```
142
+
143
+ Business logic lives in subscribers — testable without React:
144
+
145
+ ```js
146
+ store.subscribe('intent.addTask', (text) => {
147
+ const tasks = store.get('state.tasks') || [];
148
+ store.set('state.tasks', [...tasks, { id: genId(), text }]);
149
+ });
150
+ ```
151
+
152
+ ## Why a separate package?
153
+
154
+ - **Zero cost if you don't use React** — `@uistate/core` stays framework-free
155
+ - **React is a peer dependency** — not bundled, no version conflicts
156
+ - **Tiny** — ~50 lines of code, no dependencies beyond React and the core store
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,127 @@
1
+ import { createContext, useContext, useMemo, useSyncExternalStore } from 'react';
2
+
3
+ // ---- Context ----
4
+ const EventStateContext = createContext(null);
5
+
6
+ /**
7
+ * Provider — makes a store available to all child components via hooks.
8
+ * The store is created *outside* React. The provider is pure dependency injection.
9
+ *
10
+ * @param {{ store: object, children: React.ReactNode }} props
11
+ */
12
+ export function EventStateProvider({ store, children }) {
13
+ return (
14
+ <EventStateContext.Provider value={store}>
15
+ {children}
16
+ </EventStateContext.Provider>
17
+ );
18
+ }
19
+
20
+ /**
21
+ * useStore — returns the EventState store from context.
22
+ * Throws if called outside an EventStateProvider.
23
+ *
24
+ * @returns {object} The EventState store
25
+ */
26
+ export function useStore() {
27
+ const store = useContext(EventStateContext);
28
+ if (!store) {
29
+ throw new Error(
30
+ 'useStore: no store found. Wrap your component tree in <EventStateProvider store={store}>.'
31
+ );
32
+ }
33
+ return store;
34
+ }
35
+
36
+ /**
37
+ * usePath — subscribe to a dot-path in the store.
38
+ * Re-renders the component only when the value at that path changes.
39
+ * Uses React 18's useSyncExternalStore for concurrent-mode safety.
40
+ *
41
+ * @param {string} path — dot-separated state path (e.g. 'state.tasks')
42
+ * @returns {any} The current value at the path
43
+ */
44
+ export function usePath(path) {
45
+ const store = useStore();
46
+
47
+ const subscribe = useMemo(
48
+ () => (onStoreChange) => store.subscribe(path, () => onStoreChange()),
49
+ [store, path]
50
+ );
51
+
52
+ const getSnapshot = useMemo(
53
+ () => () => store.get(path),
54
+ [store, path]
55
+ );
56
+
57
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
58
+ }
59
+
60
+ /**
61
+ * useIntent — returns a stable function that publishes a value to a path.
62
+ * Memoized so it won't cause unnecessary re-renders when passed as a prop.
63
+ *
64
+ * @param {string} path — dot-separated intent path (e.g. 'intent.addTask')
65
+ * @returns {(value: any) => any} A setter function
66
+ */
67
+ export function useIntent(path) {
68
+ const store = useStore();
69
+ return useMemo(
70
+ () => (value) => store.set(path, value),
71
+ [store, path]
72
+ );
73
+ }
74
+
75
+ /**
76
+ * useWildcard — subscribe to a wildcard path (e.g. 'state.*').
77
+ * Re-renders whenever any child of that path changes.
78
+ * The returned value is the parent object at the path prefix.
79
+ *
80
+ * @param {string} wildcardPath — e.g. 'state.tasks.*' or 'state.*'
81
+ * @returns {any} The current value at the parent path
82
+ */
83
+ export function useWildcard(wildcardPath) {
84
+ const store = useStore();
85
+ const parentPath = wildcardPath.endsWith('.*')
86
+ ? wildcardPath.slice(0, -2)
87
+ : wildcardPath;
88
+
89
+ const subscribe = useMemo(
90
+ () => (onStoreChange) => store.subscribe(wildcardPath, () => onStoreChange()),
91
+ [store, wildcardPath]
92
+ );
93
+
94
+ const getSnapshot = useMemo(
95
+ () => () => store.get(parentPath),
96
+ [store, parentPath]
97
+ );
98
+
99
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
100
+ }
101
+
102
+ /**
103
+ * useAsync — trigger an async operation and subscribe to its status.
104
+ * Returns { data, status, error, execute, cancel }.
105
+ *
106
+ * @param {string} path — base path for the async operation
107
+ * @returns {{ data: any, status: string, error: any, execute: Function, cancel: Function }}
108
+ */
109
+ export function useAsync(path) {
110
+ const store = useStore();
111
+
112
+ const data = usePath(`${path}.data`);
113
+ const status = usePath(`${path}.status`);
114
+ const error = usePath(`${path}.error`);
115
+
116
+ const execute = useMemo(
117
+ () => (fetcher) => store.setAsync(path, fetcher),
118
+ [store, path]
119
+ );
120
+
121
+ const cancel = useMemo(
122
+ () => () => store.cancel(path),
123
+ [store, path]
124
+ );
125
+
126
+ return { data, status, error, execute, cancel };
127
+ }
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export {
2
+ EventStateProvider,
3
+ useStore,
4
+ usePath,
5
+ useIntent,
6
+ useWildcard,
7
+ useAsync,
8
+ } from './eventStateReact.js';
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@uistate/react",
3
+ "version": "1.0.0",
4
+ "description": "React adapter for @uistate/core — usePath, useIntent, useAsync hooks and EventStateProvider",
5
+ "main": "index.js",
6
+ "module": "index.js",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.js",
11
+ "default": "./index.js"
12
+ },
13
+ "./eventStateReact": {
14
+ "import": "./eventStateReact.js",
15
+ "default": "./eventStateReact.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "index.js",
20
+ "eventStateReact.js",
21
+ "README.md"
22
+ ],
23
+ "peerDependencies": {
24
+ "@uistate/core": ">=5.0.0",
25
+ "react": ">=18.0.0"
26
+ },
27
+ "keywords": [
28
+ "uistate",
29
+ "react",
30
+ "state-management",
31
+ "hooks",
32
+ "useSyncExternalStore",
33
+ "event-driven",
34
+ "dot-path",
35
+ "external-store",
36
+ "adapter"
37
+ ],
38
+ "author": "Ajdin Imsirovic",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/ImsirovicAjdin/uistate-react.git"
43
+ },
44
+ "homepage": "https://uistate.com/react.html",
45
+ "bugs": {
46
+ "url": "https://github.com/ImsirovicAjdin/uistate-react/issues"
47
+ }
48
+ }