jac-client 0.1.0__py3-none-any.whl
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.
- jac_client/docs/README.md +629 -0
- jac_client/docs/advanced-state.md +706 -0
- jac_client/docs/imports.md +650 -0
- jac_client/docs/lifecycle-hooks.md +554 -0
- jac_client/docs/routing.md +530 -0
- jac_client/examples/little-x/app.jac +615 -0
- jac_client/examples/little-x/package-lock.json +2840 -0
- jac_client/examples/little-x/package.json +23 -0
- jac_client/examples/little-x/submit-button.jac +8 -0
- jac_client/examples/todo-app/README.md +82 -0
- jac_client/examples/todo-app/app.jac +683 -0
- jac_client/examples/todo-app/package-lock.json +999 -0
- jac_client/examples/todo-app/package.json +22 -0
- jac_client/plugin/cli.py +328 -0
- jac_client/plugin/client.py +41 -0
- jac_client/plugin/client_runtime.jac +941 -0
- jac_client/plugin/vite_client_bundle.py +470 -0
- jac_client/tests/__init__.py +2 -0
- jac_client/tests/fixtures/button.jac +6 -0
- jac_client/tests/fixtures/client_app.jac +18 -0
- jac_client/tests/fixtures/client_app_with_antd.jac +21 -0
- jac_client/tests/fixtures/js_import.jac +30 -0
- jac_client/tests/fixtures/package-lock.json +329 -0
- jac_client/tests/fixtures/package.json +11 -0
- jac_client/tests/fixtures/relative_import.jac +13 -0
- jac_client/tests/fixtures/test_fragments_spread.jac +44 -0
- jac_client/tests/fixtures/utils.js +22 -0
- jac_client/tests/test_cl.py +360 -0
- jac_client/tests/test_create_jac_app.py +139 -0
- jac_client-0.1.0.dist-info/METADATA +126 -0
- jac_client-0.1.0.dist-info/RECORD +33 -0
- jac_client-0.1.0.dist-info/WHEEL +4 -0
- jac_client-0.1.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
# Advanced State Management in Jac
|
|
2
|
+
|
|
3
|
+
Learn how to combine multiple `createState()` calls, manage complex state patterns, and build scalable state architectures.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📚 Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Multiple State Instances](#multiple-state-instances)
|
|
10
|
+
- [State Composition Patterns](#state-composition-patterns)
|
|
11
|
+
- [Derived State](#derived-state)
|
|
12
|
+
- [State Management Patterns](#state-management-patterns)
|
|
13
|
+
- [Best Practices](#best-practices)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Multiple State Instances
|
|
18
|
+
|
|
19
|
+
### Basic Multiple State Pattern
|
|
20
|
+
|
|
21
|
+
Instead of putting everything in one state object, you can split state into multiple instances:
|
|
22
|
+
|
|
23
|
+
```jac
|
|
24
|
+
cl {
|
|
25
|
+
# Separate concerns into different state instances
|
|
26
|
+
let [todoState, setTodoState] = createState({
|
|
27
|
+
"items": []
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let [filterState, setFilterState] = createState({
|
|
31
|
+
"filter": "all"
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let [uiState, setUiState] = createState({
|
|
35
|
+
"loading": False,
|
|
36
|
+
"error": None
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
def TodoApp() -> any {
|
|
40
|
+
todos = todoState();
|
|
41
|
+
filter = filterState();
|
|
42
|
+
ui = uiState();
|
|
43
|
+
|
|
44
|
+
return <div>
|
|
45
|
+
{ui.loading and <div>Loading...</div>}
|
|
46
|
+
{ui.error and <div>Error: {ui.error}</div>}
|
|
47
|
+
{[TodoItem(item) for item in todos.items]}
|
|
48
|
+
</div>;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Benefits:**
|
|
54
|
+
- **Separation of Concerns**: Each state manages one aspect
|
|
55
|
+
- **Selective Updates**: Only components using specific state re-render
|
|
56
|
+
- **Easier Testing**: Test each state independently
|
|
57
|
+
- **Better Organization**: Clearer code structure
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## State Composition Patterns
|
|
62
|
+
|
|
63
|
+
### Pattern 1: Feature-Based State
|
|
64
|
+
|
|
65
|
+
Organize state by feature or domain:
|
|
66
|
+
|
|
67
|
+
```jac
|
|
68
|
+
cl {
|
|
69
|
+
# User state
|
|
70
|
+
let [userState, setUserState] = createState({
|
|
71
|
+
"profile": None,
|
|
72
|
+
"isLoggedIn": False
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
# Todo state
|
|
76
|
+
let [todoState, setTodoState] = createState({
|
|
77
|
+
"items": [],
|
|
78
|
+
"selectedId": None
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
# UI state
|
|
82
|
+
let [uiState, setUiState] = createState({
|
|
83
|
+
"theme": "light",
|
|
84
|
+
"sidebarOpen": False,
|
|
85
|
+
"modalOpen": False
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
# Settings state
|
|
89
|
+
let [settingsState, setSettingsState] = createState({
|
|
90
|
+
"notifications": True,
|
|
91
|
+
"language": "en"
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
def App() -> any {
|
|
95
|
+
user = userState();
|
|
96
|
+
todos = todoState();
|
|
97
|
+
ui = uiState();
|
|
98
|
+
settings = settingsState();
|
|
99
|
+
|
|
100
|
+
return <div className={ui.theme}>
|
|
101
|
+
{ui.sidebarOpen and <Sidebar />}
|
|
102
|
+
{todos.items.length > 0 and <TodoList items={todos.items} />}
|
|
103
|
+
</div>;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Pattern 2: Local vs Global State
|
|
109
|
+
|
|
110
|
+
Use different state instances for different scopes:
|
|
111
|
+
|
|
112
|
+
```jac
|
|
113
|
+
cl {
|
|
114
|
+
# Global application state
|
|
115
|
+
let [appState, setAppState] = createState({
|
|
116
|
+
"currentUser": None,
|
|
117
|
+
"theme": "light"
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
# Component-specific state (can be defined inside components)
|
|
121
|
+
def TodoForm() -> any {
|
|
122
|
+
# Local component state
|
|
123
|
+
let [formState, setFormState] = createState({
|
|
124
|
+
"text": "",
|
|
125
|
+
"valid": False
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
def validate() -> None {
|
|
129
|
+
text = formState().text;
|
|
130
|
+
setFormState({"valid": len(text.trim()) > 0});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return <form>
|
|
134
|
+
<input
|
|
135
|
+
value={formState().text}
|
|
136
|
+
onChange={lambda e: any -> None {
|
|
137
|
+
setFormState({"text": e.target.value});
|
|
138
|
+
validate();
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</form>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
def TodoList() -> any {
|
|
145
|
+
# Local list state
|
|
146
|
+
let [listState, setListState] = createState({
|
|
147
|
+
"sortBy": "date",
|
|
148
|
+
"order": "asc"
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
todos = appState().todos;
|
|
152
|
+
sorted = sortTodos(todos, listState());
|
|
153
|
+
|
|
154
|
+
return <div>
|
|
155
|
+
{[TodoItem(item) for item in sorted]}
|
|
156
|
+
</div>;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Pattern 3: State Modules
|
|
162
|
+
|
|
163
|
+
Create reusable state modules:
|
|
164
|
+
|
|
165
|
+
```jac
|
|
166
|
+
cl {
|
|
167
|
+
# State module: User management
|
|
168
|
+
let [userState, setUserState] = createState({
|
|
169
|
+
"currentUser": None,
|
|
170
|
+
"isLoading": False,
|
|
171
|
+
"error": None
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
async def loadUser() -> None {
|
|
175
|
+
setUserState({"isLoading": True, "error": None});
|
|
176
|
+
try {
|
|
177
|
+
user = await __jacSpawn("get_current_user");
|
|
178
|
+
setUserState({"currentUser": user, "isLoading": False});
|
|
179
|
+
} except Exception as err {
|
|
180
|
+
setUserState({"error": str(err), "isLoading": False});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def logout() -> None {
|
|
185
|
+
jacLogout();
|
|
186
|
+
setUserState({"currentUser": None});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# State module: Todo management
|
|
190
|
+
let [todoState, setTodoState] = createState({
|
|
191
|
+
"items": [],
|
|
192
|
+
"filter": "all",
|
|
193
|
+
"loading": False
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
async def loadTodos() -> None {
|
|
197
|
+
setTodoState({"loading": True});
|
|
198
|
+
try {
|
|
199
|
+
todos = await __jacSpawn("read_todos");
|
|
200
|
+
setTodoState({"items": todos.reports, "loading": False});
|
|
201
|
+
} except Exception as err {
|
|
202
|
+
setTodoState({"loading": False});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async def addTodo(text: str) -> None {
|
|
207
|
+
new_todo = await __jacSpawn("create_todo", {"text": text});
|
|
208
|
+
s = todoState();
|
|
209
|
+
setTodoState({"items": s.items.concat([new_todo])});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Derived State
|
|
217
|
+
|
|
218
|
+
### Computed Values from Multiple States
|
|
219
|
+
|
|
220
|
+
Combine multiple states to create derived values:
|
|
221
|
+
|
|
222
|
+
```jac
|
|
223
|
+
cl {
|
|
224
|
+
let [todoState, setTodoState] = createState({
|
|
225
|
+
"items": []
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
let [filterState, setFilterState] = createState({
|
|
229
|
+
"filter": "all"
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
def getFilteredTodos() -> list {
|
|
233
|
+
todos = todoState();
|
|
234
|
+
filter = filterState();
|
|
235
|
+
|
|
236
|
+
if filter == "active" {
|
|
237
|
+
return [item for item in todos.items if not item.done];
|
|
238
|
+
} elif filter == "completed" {
|
|
239
|
+
return [item for item in todos.items if item.done];
|
|
240
|
+
}
|
|
241
|
+
return todos.items;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
def getStats() -> dict {
|
|
245
|
+
todos = todoState();
|
|
246
|
+
total = len(todos.items);
|
|
247
|
+
active = len([item for item in todos.items if not item.done]);
|
|
248
|
+
completed = total - active;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"total": total,
|
|
252
|
+
"active": active,
|
|
253
|
+
"completed": completed
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
def TodoApp() -> any {
|
|
258
|
+
filtered = getFilteredTodos();
|
|
259
|
+
stats = getStats();
|
|
260
|
+
|
|
261
|
+
return <div>
|
|
262
|
+
<div>
|
|
263
|
+
Total: {stats.total}, Active: {stats.active}, Completed: {stats.completed}
|
|
264
|
+
</div>
|
|
265
|
+
{[TodoItem(item) for item in filtered]}
|
|
266
|
+
</div>;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Reactive Derived State
|
|
272
|
+
|
|
273
|
+
Use `createEffect()` for reactive derived state:
|
|
274
|
+
|
|
275
|
+
```jac
|
|
276
|
+
cl {
|
|
277
|
+
let [todoState, setTodoState] = createState({
|
|
278
|
+
"items": []
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let [statsState, setStatsState] = createState({
|
|
282
|
+
"total": 0,
|
|
283
|
+
"active": 0,
|
|
284
|
+
"completed": 0
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
# Automatically update stats when todos change
|
|
288
|
+
createEffect(lambda -> None {
|
|
289
|
+
todos = todoState();
|
|
290
|
+
total = len(todos.items);
|
|
291
|
+
active = len([item for item in todos.items if not item.done]);
|
|
292
|
+
completed = total - active;
|
|
293
|
+
|
|
294
|
+
setStatsState({
|
|
295
|
+
"total": total,
|
|
296
|
+
"active": active,
|
|
297
|
+
"completed": completed
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
def TodoApp() -> any {
|
|
302
|
+
todos = todoState();
|
|
303
|
+
stats = statsState();
|
|
304
|
+
|
|
305
|
+
return <div>
|
|
306
|
+
<StatsDisplay stats={stats} />
|
|
307
|
+
{[TodoItem(item) for item in todos.items]}
|
|
308
|
+
</div>;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## State Management Patterns
|
|
316
|
+
|
|
317
|
+
### Pattern 1: State Reducers
|
|
318
|
+
|
|
319
|
+
Create reducer-like functions for complex state updates:
|
|
320
|
+
|
|
321
|
+
```jac
|
|
322
|
+
cl {
|
|
323
|
+
let [todoState, setTodoState] = createState({
|
|
324
|
+
"items": [],
|
|
325
|
+
"filter": "all",
|
|
326
|
+
"input": ""
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
def todoReducer(action: str, payload: any) -> None {
|
|
330
|
+
s = todoState();
|
|
331
|
+
|
|
332
|
+
if action == "ADD_TODO" {
|
|
333
|
+
newItem = {
|
|
334
|
+
"id": payload.id,
|
|
335
|
+
"text": payload.text,
|
|
336
|
+
"done": False
|
|
337
|
+
};
|
|
338
|
+
setTodoState({"items": s.items.concat([newItem])});
|
|
339
|
+
|
|
340
|
+
} elif action == "TOGGLE_TODO" {
|
|
341
|
+
updated = [item for item in s.items {
|
|
342
|
+
if item.id == payload.id {
|
|
343
|
+
return {"id": item.id, "text": item.text, "done": not item.done};
|
|
344
|
+
}
|
|
345
|
+
return item;
|
|
346
|
+
}];
|
|
347
|
+
setTodoState({"items": updated});
|
|
348
|
+
|
|
349
|
+
} elif action == "REMOVE_TODO" {
|
|
350
|
+
remaining = [item for item in s.items if item.id != payload.id];
|
|
351
|
+
setTodoState({"items": remaining});
|
|
352
|
+
|
|
353
|
+
} elif action == "SET_FILTER" {
|
|
354
|
+
setTodoState({"filter": payload.filter});
|
|
355
|
+
|
|
356
|
+
} elif action == "CLEAR_COMPLETED" {
|
|
357
|
+
remaining = [item for item in s.items if not item.done];
|
|
358
|
+
setTodoState({"items": remaining});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async def addTodo(text: str) -> None {
|
|
363
|
+
new_todo = await __jacSpawn("create_todo", {"text": text});
|
|
364
|
+
todoReducer("ADD_TODO", {
|
|
365
|
+
"id": new_todo._jac_id,
|
|
366
|
+
"text": new_todo.text
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async def toggleTodo(id: str) -> None {
|
|
371
|
+
await __jacSpawn("toggle_todo", {}, id);
|
|
372
|
+
todoReducer("TOGGLE_TODO", {"id": id});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
def setFilter(filter: str) -> None {
|
|
376
|
+
todoReducer("SET_FILTER", {"filter": filter});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Pattern 2: State Selectors
|
|
382
|
+
|
|
383
|
+
Create selector functions to extract specific state slices:
|
|
384
|
+
|
|
385
|
+
```jac
|
|
386
|
+
cl {
|
|
387
|
+
let [todoState, setTodoState] = createState({
|
|
388
|
+
"items": [],
|
|
389
|
+
"filter": "all",
|
|
390
|
+
"loading": False,
|
|
391
|
+
"error": None
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
# Selectors
|
|
395
|
+
def getTodos() -> list {
|
|
396
|
+
return todoState().items;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
def getActiveTodos() -> list {
|
|
400
|
+
return [item for item in todoState().items if not item.done];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
def getCompletedTodos() -> list {
|
|
404
|
+
return [item for item in todoState().items if item.done];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
def getFilteredTodos() -> list {
|
|
408
|
+
s = todoState();
|
|
409
|
+
if s.filter == "active" {
|
|
410
|
+
return getActiveTodos();
|
|
411
|
+
} elif s.filter == "completed" {
|
|
412
|
+
return getCompletedTodos();
|
|
413
|
+
}
|
|
414
|
+
return getTodos();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
def isLoading() -> bool {
|
|
418
|
+
return todoState().loading;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
def getError() -> str | None {
|
|
422
|
+
return todoState().error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Pattern 3: State Actions
|
|
428
|
+
|
|
429
|
+
Create action functions that encapsulate state updates:
|
|
430
|
+
|
|
431
|
+
```jac
|
|
432
|
+
cl {
|
|
433
|
+
let [todoState, setTodoState] = createState({
|
|
434
|
+
"items": [],
|
|
435
|
+
"filter": "all"
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
# Action creators
|
|
439
|
+
async def createTodoAction(text: str) -> None {
|
|
440
|
+
if not text.trim() {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
new_todo = await __jacSpawn("create_todo", {"text": text});
|
|
445
|
+
s = todoState();
|
|
446
|
+
newItem = {
|
|
447
|
+
"id": new_todo._jac_id,
|
|
448
|
+
"text": new_todo.text,
|
|
449
|
+
"done": new_todo.done
|
|
450
|
+
};
|
|
451
|
+
setTodoState({"items": s.items.concat([newItem])});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async def toggleTodoAction(id: str) -> None {
|
|
455
|
+
await __jacSpawn("toggle_todo", {}, id);
|
|
456
|
+
s = todoState();
|
|
457
|
+
updated = [item for item in s.items {
|
|
458
|
+
if item.id == id {
|
|
459
|
+
return {"id": item.id, "text": item.text, "done": not item.done};
|
|
460
|
+
}
|
|
461
|
+
return item;
|
|
462
|
+
}];
|
|
463
|
+
setTodoState({"items": updated});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
def removeTodoAction(id: str) -> None {
|
|
467
|
+
s = todoState();
|
|
468
|
+
remaining = [item for item in s.items if item.id != id];
|
|
469
|
+
setTodoState({"items": remaining});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
def setFilterAction(filter: str) -> None {
|
|
473
|
+
setTodoState({"filter": filter});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
def clearCompletedAction() -> None {
|
|
477
|
+
s = todoState();
|
|
478
|
+
remaining = [item for item in s.items if not item.done];
|
|
479
|
+
setTodoState({"items": remaining});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Complete Example: Multi-State Todo App
|
|
487
|
+
|
|
488
|
+
Here's a complete example combining multiple state patterns:
|
|
489
|
+
|
|
490
|
+
```jac
|
|
491
|
+
cl {
|
|
492
|
+
# User state
|
|
493
|
+
let [userState, setUserState] = createState({
|
|
494
|
+
"profile": None,
|
|
495
|
+
"isLoading": False
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
# Todo state
|
|
499
|
+
let [todoState, setTodoState] = createState({
|
|
500
|
+
"items": [],
|
|
501
|
+
"loading": False,
|
|
502
|
+
"error": None
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
# Filter state
|
|
506
|
+
let [filterState, setFilterState] = createState({
|
|
507
|
+
"filter": "all"
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
# UI state
|
|
511
|
+
let [uiState, setUiState] = createState({
|
|
512
|
+
"sidebarOpen": False,
|
|
513
|
+
"modalOpen": False
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
# Derived state functions
|
|
517
|
+
def getFilteredTodos() -> list {
|
|
518
|
+
todos = todoState();
|
|
519
|
+
filter = filterState();
|
|
520
|
+
|
|
521
|
+
if filter == "active" {
|
|
522
|
+
return [item for item in todos.items if not item.done];
|
|
523
|
+
} elif filter == "completed" {
|
|
524
|
+
return [item for item in todos.items if item.done];
|
|
525
|
+
}
|
|
526
|
+
return todos.items;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
def getStats() -> dict {
|
|
530
|
+
todos = todoState();
|
|
531
|
+
total = len(todos.items);
|
|
532
|
+
active = len([item for item in todos.items if not item.done]);
|
|
533
|
+
completed = total - active;
|
|
534
|
+
|
|
535
|
+
return {"total": total, "active": active, "completed": completed};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
# Actions
|
|
539
|
+
async def loadTodos() -> None {
|
|
540
|
+
setTodoState({"loading": True, "error": None});
|
|
541
|
+
try {
|
|
542
|
+
todos = await __jacSpawn("read_todos");
|
|
543
|
+
items = [{
|
|
544
|
+
"id": todo._jac_id,
|
|
545
|
+
"text": todo.text,
|
|
546
|
+
"done": todo.done
|
|
547
|
+
} for todo in todos.reports];
|
|
548
|
+
setTodoState({"items": items, "loading": False});
|
|
549
|
+
} except Exception as err {
|
|
550
|
+
setTodoState({"error": str(err), "loading": False});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async def addTodo(text: str) -> None {
|
|
555
|
+
new_todo = await __jacSpawn("create_todo", {"text": text});
|
|
556
|
+
s = todoState();
|
|
557
|
+
newItem = {
|
|
558
|
+
"id": new_todo._jac_id,
|
|
559
|
+
"text": new_todo.text,
|
|
560
|
+
"done": new_todo.done
|
|
561
|
+
};
|
|
562
|
+
setTodoState({"items": s.items.concat([newItem])});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
def setFilter(filter: str) -> None {
|
|
566
|
+
setFilterState({"filter": filter});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
def toggleSidebar() -> None {
|
|
570
|
+
s = uiState();
|
|
571
|
+
setUiState({"sidebarOpen": not s.sidebarOpen});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# Components
|
|
575
|
+
def TodoApp() -> any {
|
|
576
|
+
onMount(lambda -> None {
|
|
577
|
+
loadTodos();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
todos = todoState();
|
|
581
|
+
filter = filterState();
|
|
582
|
+
ui = uiState();
|
|
583
|
+
|
|
584
|
+
if todos.loading {
|
|
585
|
+
return <div>Loading...</div>;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if todos.error {
|
|
589
|
+
return <div>Error: {todos.error}</div>;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
filtered = getFilteredTodos();
|
|
593
|
+
stats = getStats();
|
|
594
|
+
|
|
595
|
+
return <div>
|
|
596
|
+
<Header stats={stats} onToggleSidebar={toggleSidebar} />
|
|
597
|
+
{ui.sidebarOpen and <Sidebar filter={filter} onSetFilter={setFilter} />}
|
|
598
|
+
<TodoList items={filtered} />
|
|
599
|
+
</div>;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## Best Practices
|
|
607
|
+
|
|
608
|
+
### 1. Separate Concerns
|
|
609
|
+
|
|
610
|
+
```jac
|
|
611
|
+
# ✅ Good: Separate state by concern
|
|
612
|
+
let [userState, setUserState] = createState({"profile": None});
|
|
613
|
+
let [todoState, setTodoState] = createState({"items": []});
|
|
614
|
+
let [uiState, setUiState] = createState({"sidebarOpen": False});
|
|
615
|
+
|
|
616
|
+
# ❌ Avoid: Mixing unrelated state
|
|
617
|
+
let [appState, setAppState] = createState({
|
|
618
|
+
"user": None,
|
|
619
|
+
"todos": [],
|
|
620
|
+
"sidebarOpen": False,
|
|
621
|
+
"theme": "light"
|
|
622
|
+
});
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### 2. Keep State Flat
|
|
626
|
+
|
|
627
|
+
```jac
|
|
628
|
+
# ✅ Good: Flat state structure
|
|
629
|
+
let [todoState, setTodoState] = createState({
|
|
630
|
+
"items": [],
|
|
631
|
+
"loading": False
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
# ❌ Avoid: Deeply nested state
|
|
635
|
+
let [appState, setAppState] = createState({
|
|
636
|
+
"data": {
|
|
637
|
+
"todos": {
|
|
638
|
+
"items": [],
|
|
639
|
+
"meta": {
|
|
640
|
+
"loading": False
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### 3. Use Selectors for Computed Values
|
|
648
|
+
|
|
649
|
+
```jac
|
|
650
|
+
# ✅ Good: Computed values via functions
|
|
651
|
+
def getActiveTodos() -> list {
|
|
652
|
+
return [item for item in todoState().items if not item.done];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
# ❌ Avoid: Storing computed values in state
|
|
656
|
+
let [todoState, setTodoState] = createState({
|
|
657
|
+
"items": [],
|
|
658
|
+
"activeTodos": [] # Computed, shouldn't be in state
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### 4. Encapsulate State Updates
|
|
663
|
+
|
|
664
|
+
```jac
|
|
665
|
+
# ✅ Good: Action functions
|
|
666
|
+
async def addTodo(text: str) -> None {
|
|
667
|
+
new_todo = await __jacSpawn("create_todo", {"text": text});
|
|
668
|
+
# Update state
|
|
669
|
+
setTodoState(/* ... */);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
# ❌ Avoid: Direct state updates everywhere
|
|
673
|
+
def Component() -> any {
|
|
674
|
+
# Don't call setTodoState directly here
|
|
675
|
+
# Use action functions instead
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### 5. Handle Loading and Error States
|
|
680
|
+
|
|
681
|
+
```jac
|
|
682
|
+
# ✅ Good: Include loading/error in state
|
|
683
|
+
let [todoState, setTodoState] = createState({
|
|
684
|
+
"items": [],
|
|
685
|
+
"loading": False,
|
|
686
|
+
"error": None
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
# ❌ Avoid: Missing error handling
|
|
690
|
+
let [todoState, setTodoState] = createState({
|
|
691
|
+
"items": []
|
|
692
|
+
});
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## Summary
|
|
698
|
+
|
|
699
|
+
- **Multiple States**: Split state by concern for better organization
|
|
700
|
+
- **State Composition**: Combine multiple states for complex applications
|
|
701
|
+
- **Derived State**: Compute values from multiple states using functions
|
|
702
|
+
- **State Patterns**: Use reducers, selectors, and actions for complex state
|
|
703
|
+
- **Best Practices**: Keep state flat, separate concerns, handle errors
|
|
704
|
+
|
|
705
|
+
Advanced state management helps you build scalable, maintainable applications! 🚀
|
|
706
|
+
|