@coherent.js/state 1.1.2 → 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 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@beta
8
+ npm install @coherent.js/state
9
9
  # or
10
- pnpm add @coherent.js/state@beta
10
+ pnpm add @coherent.js/state
11
11
  # or
12
- yarn add @coherent.js/state@beta
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
- ### SSR-Compatible State
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
- import { createState, provideContext } from '@coherent.js/state';
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
- // Create state container for a request
51
- const state = createState({ userId: 123, theme: 'dark' });
59
+ ### SSR-Compatible State
52
60
 
53
- // Provide context during SSR
54
- provideContext('request', state);
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: 0 },
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.set('email', 'invalid-email'); // Throws validation error
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.