r-state-tree 0.3.0 → 0.4.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
@@ -8,13 +8,18 @@ Reactive state management featuring store trees, computed child stores, and snap
8
8
  pnpm add r-state-tree
9
9
  ```
10
10
 
11
+ ### Requirements
12
+
13
+ This library uses [TC39 Stage 3 Decorators](https://github.com/tc39/proposal-decorators) and requires TypeScript 5.0+ with `target: "es2022"` or higher.
14
+
15
+ The library includes a decorator metadata polyfill for runtimes that don't yet natively support `Symbol.metadata`.
16
+
11
17
  ## Stores
12
18
 
13
19
  Stores describe reactive state containers composed into a tree.
14
20
 
15
21
  ```ts
16
- import Store, { createStore, mount } from "r-state-tree";
17
- import { child } from "r-state-tree/decorators";
22
+ import { Store, createStore, mount, child } from "r-state-tree";
18
23
 
19
24
  class TodoStore extends Store {
20
25
  props = { title: "" };
@@ -44,58 +49,278 @@ import { updateStore } from "r-state-tree";
44
49
  updateStore(app.todo, { title: "Ship release" });
45
50
  ```
46
51
 
52
+ ### Child stores
53
+
54
+ Use `@child` for single child stores and `@children` for arrays.
55
+
56
+ ```ts
57
+ import { child, children } from "r-state-tree";
58
+
59
+ class ListStore extends Store {
60
+ items = ["Buy milk", "Walk dog"];
61
+
62
+ @children get todos() {
63
+ return this.items.map((title, i) =>
64
+ createStore(TodoStore, { title, key: i })
65
+ );
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### Lifecycle hooks
71
+
72
+ Stores support lifecycle methods:
73
+
74
+ ```ts
75
+ class TodoStore extends Store {
76
+ storeDidMount() {
77
+ console.log("Store mounted");
78
+ }
79
+
80
+ storeWillUnmount() {
81
+ console.log("Store will unmount");
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### Reactions
87
+
88
+ Create side effects that run when reactive values change:
89
+
90
+ ```ts
91
+ class TodoStore extends Store {
92
+ storeDidMount() {
93
+ this.reaction(
94
+ () => this.props.title,
95
+ (title) => console.log("Title changed:", title)
96
+ );
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### Context
102
+
103
+ Share data across the store tree without prop drilling:
104
+
105
+ ```ts
106
+ import { createContext } from "r-state-tree";
107
+
108
+ const ThemeContext = createContext<"light" | "dark">("light");
109
+
110
+ class AppStore extends Store {
111
+ theme = "dark";
112
+
113
+ [ThemeContext.provide]() {
114
+ return this.theme;
115
+ }
116
+
117
+ @child get todo() {
118
+ return createStore(TodoStore);
119
+ }
120
+ }
121
+
122
+ class TodoStore extends Store {
123
+ get theme() {
124
+ return ThemeContext.consume(this);
125
+ }
126
+ }
127
+
128
+ const app = mount(createStore(AppStore));
129
+ app.todo.theme; // "dark"
130
+ ```
131
+
132
+ Context is reactive and updates automatically when the provided value changes.
133
+
47
134
  ### Actions and batching
48
135
 
49
- `runInBatch` groups updates to avoid redundant reactions.
136
+ `batch` groups updates to avoid redundant reactions.
50
137
 
51
138
  ```ts
52
- import { runInBatch } from "r-state-tree";
139
+ import { batch } from "r-state-tree";
53
140
 
54
- runInBatch(() => {
141
+ batch(() => {
55
142
  app.todo.props.title = "Refactor";
56
143
  });
57
144
  ```
58
145
 
146
+ ## Observable classes
147
+
148
+ For reactive class instances outside the Model/Store system, use `@observable` and `@computed` decorators:
149
+
150
+ ```ts
151
+ import { Observable, observable, computed, effect } from "r-state-tree";
152
+
153
+ class Counter extends Observable {
154
+ @observable count = 0;
155
+ @observable step = 1;
156
+
157
+ @computed get doubled() {
158
+ return this.count * 2;
159
+ }
160
+
161
+ increment() {
162
+ this.count += this.step;
163
+ }
164
+ }
165
+
166
+ const counter = new Counter();
167
+
168
+ effect(() => {
169
+ console.log("Count:", counter.count, "Doubled:", counter.doubled);
170
+ });
171
+
172
+ counter.increment(); // Triggers the effect
173
+ ```
174
+
175
+ **Important:** Class instances require explicit `@observable` and `@computed` decorators. Properties without decorators are **not** reactive.
176
+
177
+ ### Plain objects (implicit reactivity)
178
+
179
+ Plain objects wrapped with `observable()` use implicit reactivity for backward compatibility:
180
+
181
+ ```ts
182
+ import { observable, effect } from "r-state-tree";
183
+
184
+ const state = observable({ count: 0 });
185
+
186
+ effect(() => {
187
+ console.log(state.count); // All properties are reactive
188
+ });
189
+
190
+ state.count++; // Triggers the effect
191
+ ```
192
+
59
193
  ## Models and snapshots
60
194
 
61
195
  Models capture persistent state with snapshot utilities.
62
196
 
63
197
  ```ts
64
- import Model from "r-state-tree/model/Model";
65
198
  import {
199
+ Model,
200
+ state,
201
+ identifier,
66
202
  applySnapshot,
67
203
  onSnapshot,
68
- onSnapshotDiff,
69
204
  toSnapshot,
70
205
  } from "r-state-tree";
71
206
 
72
207
  class TodoModel extends Model {
73
- static types = {
74
- title: stateType,
75
- assignee: modelRefType,
76
- };
77
-
78
- title = "";
208
+ @identifier id = 0;
209
+ @state title = "";
210
+ @state completed = false;
79
211
  }
80
212
 
81
- const todo = new TodoModel();
213
+ const todo = TodoModel.create({ id: 1, title: "Learn signals" });
82
214
 
83
215
  const stop = onSnapshot(todo, (snapshot) => {
84
- console.log(snapshot.title);
216
+ console.log(snapshot); // { id: 1, title: "Learn signals", completed: false }
85
217
  });
86
218
 
87
- applySnapshot(todo, { title: "Learn signals" });
219
+ todo.title = "Learn r-state-tree";
88
220
  stop();
89
221
  ```
90
222
 
223
+ ### Model decorators
224
+
225
+ Use decorators to configure model properties:
226
+
227
+ ```ts
228
+ import {
229
+ Model,
230
+ state,
231
+ identifier,
232
+ child,
233
+ children,
234
+ modelRef,
235
+ modelRefs,
236
+ } from "r-state-tree";
237
+
238
+ class User extends Model {
239
+ @identifier id = 0;
240
+ @state name = "";
241
+ }
242
+
243
+ class TodoModel extends Model {
244
+ @identifier id = 0;
245
+ @state title = "";
246
+ @modelRef assignee?: User; // Reference to another model by ID
247
+ @child metadata = MetadataModel.create(); // Nested child model
248
+ @children tags: TagModel[] = []; // Array of child models
249
+ }
250
+ ```
251
+
252
+ ### Model references
253
+
254
+ Reference models by ID using `@modelRef` and `@modelRefs`:
255
+
256
+ ```ts
257
+ class ProjectModel extends Model {
258
+ @identifier id = 0;
259
+ @children users: User[] = [];
260
+ @modelRef owner?: User;
261
+
262
+ assignOwner(userId: number) {
263
+ // Find user by ID and set as owner
264
+ const user = this.users.find((u) => u.id === userId);
265
+ this.owner = user;
266
+ }
267
+ }
268
+
269
+ const project = ProjectModel.create({
270
+ id: 1,
271
+ users: [
272
+ { id: 1, name: "Alice" },
273
+ { id: 2, name: "Bob" },
274
+ ],
275
+ owner: { id: 1 }, // Reference by ID in snapshot
276
+ });
277
+
278
+ project.owner?.name; // "Alice"
279
+ ```
280
+
91
281
  ### Snapshot diffs
92
282
 
93
- Use `onSnapshotDiff` to receive undo/redo payloads.
283
+ Use `onSnapshotDiff` to receive undo/redo payloads:
94
284
 
95
285
  ```ts
96
- const off = onSnapshotDiff(todo, ({ undo, redo }) => {
97
- // undo and redo contain patch-like snapshots
286
+ const history: SnapshotDiff[] = [];
287
+
288
+ const off = onSnapshotDiff(todo, (diff) => {
289
+ history.push(diff);
98
290
  });
291
+
292
+ todo.title = "New title";
293
+ todo.completed = true;
294
+
295
+ // Undo
296
+ applySnapshot(todo, history[history.length - 1].undo);
297
+
298
+ // Redo
299
+ applySnapshot(todo, history[history.length - 1].redo);
300
+ ```
301
+
302
+ ### Context with Models
303
+
304
+ Models also support context:
305
+
306
+ ```ts
307
+ const AuthContext = createContext<User | null>(null);
308
+
309
+ class AppModel extends Model {
310
+ @child currentUser = User.create({ id: 1, name: "Alice" });
311
+
312
+ [AuthContext.provide]() {
313
+ return this.currentUser;
314
+ }
315
+
316
+ @child project = ProjectModel.create();
317
+ }
318
+
319
+ class ProjectModel extends Model {
320
+ get currentUser() {
321
+ return AuthContext.consume(this);
322
+ }
323
+ }
99
324
  ```
100
325
 
101
326
  ## Testing