@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 +160 -0
- package/eventStateReact.js +127 -0
- package/index.js +8 -0
- package/package.json +48 -0
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
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
|
+
}
|