@coherent.js/state 1.1.0 → 2.0.0-rc.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 +84 -15
- package/dist/index.js +929 -335
- package/dist/index.js.map +3 -3
- package/dist/reactive-state.js +468 -111
- package/dist/reactive-state.js.map +2 -2
- package/dist/state-manager.js +141 -38
- package/dist/state-manager.js.map +3 -3
- package/dist/state-persistence.js +239 -143
- package/dist/state-persistence.js.map +2 -2
- package/dist/state-validation.js +70 -43
- package/dist/state-validation.js.map +2 -2
- package/package.json +1 -4
- package/types/index.d.ts +143 -36
package/README.md
CHANGED
|
@@ -5,11 +5,11 @@ Reactive state management for Coherent.js applications with SSR support, persist
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @coherent.js/state
|
|
8
|
+
npm install @coherent.js/state
|
|
9
9
|
# or
|
|
10
|
-
pnpm add @coherent.js/state
|
|
10
|
+
pnpm add @coherent.js/state
|
|
11
11
|
# or
|
|
12
|
-
yarn add @coherent.js/state
|
|
12
|
+
yarn add @coherent.js/state
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Features
|
|
@@ -42,57 +42,126 @@ count.value = 5; // Triggers watcher and updates computed
|
|
|
42
42
|
console.log(doubled.value); // 10
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Computed values recompute lazily and only when something they read changed;
|
|
46
|
+
reading a computed from its own getter throws. Watchers run after each write,
|
|
47
|
+
or once after `batch(() => { ... })`, each in isolation: an error is passed to
|
|
48
|
+
the `onError` option (or `globalErrorHandler`) and the others still run.
|
|
49
|
+
Writing an identical primitive notifies nobody.
|
|
50
|
+
|
|
51
|
+
`createReactiveState()` keys accept dot paths:
|
|
46
52
|
|
|
47
53
|
```javascript
|
|
48
|
-
|
|
54
|
+
const app = createReactiveState({ user: { name: 'Ada', age: 36 } });
|
|
55
|
+
app.watch('user.name', (name, previous) => console.log(previous, '→', name));
|
|
56
|
+
app.set('user.name', 'John'); // writes a copy of `user`; notifies 'user' and 'user.name'
|
|
57
|
+
```
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
const state = createState({ userId: 123, theme: 'dark' });
|
|
59
|
+
### SSR-Compatible State
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
```javascript
|
|
62
|
+
import { render } from '@coherent.js/core';
|
|
63
|
+
import {
|
|
64
|
+
createState,
|
|
65
|
+
runWithContext,
|
|
66
|
+
provideContext,
|
|
67
|
+
useContext,
|
|
68
|
+
createContextProvider
|
|
69
|
+
} from '@coherent.js/state';
|
|
70
|
+
|
|
71
|
+
// Run each request in its own context scope
|
|
72
|
+
app.get('/', (req, res) => runWithContext(async () => {
|
|
73
|
+
// Create state container for this request
|
|
74
|
+
const state = createState({ userId: req.user.id, theme: 'dark' });
|
|
75
|
+
provideContext('request', state);
|
|
76
|
+
|
|
77
|
+
const user = await loadUser(req); // context survives the await
|
|
78
|
+
res.send(render(Page(user)));
|
|
79
|
+
}));
|
|
55
80
|
|
|
56
81
|
// Access in components
|
|
57
|
-
import { useContext } from '@coherent.js/state';
|
|
58
|
-
|
|
59
82
|
function MyComponent() {
|
|
60
83
|
const requestState = useContext('request');
|
|
61
84
|
const userId = requestState.get('userId');
|
|
62
85
|
// ... render component
|
|
63
86
|
}
|
|
87
|
+
|
|
88
|
+
// Scope a value to part of the tree
|
|
89
|
+
const Page = (user) => ({
|
|
90
|
+
main: {
|
|
91
|
+
children: [
|
|
92
|
+
createContextProvider('theme', user.theme, { button: { className: () => `btn-${useContext('theme')}` } }),
|
|
93
|
+
Footer() // does not see 'theme'
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
});
|
|
64
97
|
```
|
|
65
98
|
|
|
99
|
+
On Node, context lives in `AsyncLocalStorage`: a value provided by one request
|
|
100
|
+
is never visible to another, including across `await`s. `runWithContext(fn)`
|
|
101
|
+
gives `fn` a fresh scope that ends when it returns; use it per request.
|
|
102
|
+
`provideContext()` throws outside it on Node: there the value would outlive the
|
|
103
|
+
request (a keep-alive connection carries it into the next one). A provider
|
|
104
|
+
evaluates its subtree's components with the value set, so it needs no
|
|
105
|
+
`runWithContext()` of its own. Browsers have no `AsyncLocalStorage`, so there context is only reliable for
|
|
106
|
+
synchronous rendering. `useContext(key)` falls back to `globalStateManager`
|
|
107
|
+
when no context was provided for `key`.
|
|
108
|
+
|
|
66
109
|
### State Persistence
|
|
67
110
|
|
|
68
111
|
```javascript
|
|
69
|
-
import { withLocalStorage, withSessionStorage } from '@coherent.js/state';
|
|
112
|
+
import { withLocalStorage, withSessionStorage, withIndexedDB } from '@coherent.js/state';
|
|
70
113
|
|
|
71
114
|
// Auto-persist to localStorage
|
|
72
115
|
const userPrefs = withLocalStorage({ theme: 'dark', lang: 'en' }, 'user-prefs');
|
|
73
116
|
|
|
74
117
|
// Auto-persist to sessionStorage
|
|
75
118
|
const sessionData = withSessionStorage({ cart: [] }, 'session-data');
|
|
119
|
+
|
|
120
|
+
// Auto-persist to IndexedDB: the `drafts` key of the `editor` store in the
|
|
121
|
+
// `my-app` database (defaults: 'coherent-db' and 'state')
|
|
122
|
+
const drafts = withIndexedDB({ items: [] }, 'drafts', { dbName: 'my-app', storeName: 'editor' });
|
|
123
|
+
|
|
124
|
+
// Stored state is restored asynchronously on creation
|
|
125
|
+
await userPrefs.ready;
|
|
76
126
|
```
|
|
77
127
|
|
|
128
|
+
Updates made before `ready` settles win over the stored values. Write failures
|
|
129
|
+
(for example `QuotaExceededError`) are reported through `onError`, never
|
|
130
|
+
`onSave`. `crossTab: true` syncs stores that share a key across tabs;
|
|
131
|
+
call `destroy()` when a store is no longer needed.
|
|
132
|
+
|
|
133
|
+
On the server (no `window`), browser storage backends read and write nothing —
|
|
134
|
+
Web Storage there would be shared by every request. Pass an `adapter` to
|
|
135
|
+
persist server-side.
|
|
136
|
+
|
|
137
|
+
`encrypt: true` requires an `encryptionKey` and is XOR **obfuscation**, not
|
|
138
|
+
encryption: the key ships to the browser. Never keep secrets in browser
|
|
139
|
+
storage.
|
|
140
|
+
|
|
78
141
|
### State Validation
|
|
79
142
|
|
|
80
143
|
```javascript
|
|
81
144
|
import { createValidatedState, validators } from '@coherent.js/state';
|
|
82
145
|
|
|
83
146
|
const userForm = createValidatedState(
|
|
84
|
-
{ email: '', age:
|
|
147
|
+
{ email: 'ada@example.com', age: 36 },
|
|
85
148
|
{
|
|
86
149
|
validators: {
|
|
87
|
-
email: validators.email
|
|
150
|
+
email: validators.email,
|
|
88
151
|
age: validators.range(18, 120)
|
|
89
152
|
}
|
|
90
153
|
}
|
|
91
154
|
);
|
|
92
155
|
|
|
93
|
-
userForm.
|
|
156
|
+
userForm.setState({ email: 'invalid-email' }); // invalid: not applied
|
|
157
|
+
userForm.getErrors(); // [{ path: 'email', message: 'Invalid email format', ... }]
|
|
94
158
|
```
|
|
95
159
|
|
|
160
|
+
A validator is `(value) => true | message`; `validators.email`, `url` and `required` are used
|
|
161
|
+
as is, `range(min, max)`, `length(min, max)` and `pattern(regex)` are factories. Every update
|
|
162
|
+
validates the whole resulting state, so start from a valid one. With `strict: true` an invalid
|
|
163
|
+
`setState()` throws instead.
|
|
164
|
+
|
|
96
165
|
## API Reference
|
|
97
166
|
|
|
98
167
|
See the [full documentation](https://docs.coherentjs.dev/state) for detailed API reference.
|