jac-client 0.2.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 +659 -0
- jac_client/docs/advanced-state.md +1266 -0
- jac_client/docs/assets/pipe_line.png +0 -0
- jac_client/docs/guide-example/intro.md +117 -0
- jac_client/docs/guide-example/step-01-setup.md +260 -0
- jac_client/docs/guide-example/step-02-components.md +416 -0
- jac_client/docs/guide-example/step-03-styling.md +478 -0
- jac_client/docs/guide-example/step-04-todo-ui.md +477 -0
- jac_client/docs/guide-example/step-05-local-state.md +530 -0
- jac_client/docs/guide-example/step-06-events.md +750 -0
- jac_client/docs/guide-example/step-07-effects.md +469 -0
- jac_client/docs/guide-example/step-08-walkers.md +534 -0
- jac_client/docs/guide-example/step-09-authentication.md +586 -0
- jac_client/docs/guide-example/step-10-routing.md +540 -0
- jac_client/docs/guide-example/step-11-final.md +964 -0
- jac_client/docs/imports.md +1142 -0
- jac_client/docs/lifecycle-hooks.md +774 -0
- jac_client/docs/routing.md +660 -0
- jac_client/examples/basic/.babelrc +9 -0
- jac_client/examples/basic/README.md +16 -0
- jac_client/examples/basic/app.jac +16 -0
- jac_client/examples/basic/package.json +27 -0
- jac_client/examples/basic/vite.config.js +28 -0
- jac_client/examples/basic-auth/.babelrc +9 -0
- jac_client/examples/basic-auth/README.md +16 -0
- jac_client/examples/basic-auth/app.jac +308 -0
- jac_client/examples/basic-auth/package.json +27 -0
- jac_client/examples/basic-auth/vite.config.js +28 -0
- jac_client/examples/basic-auth-with-router/.babelrc +9 -0
- jac_client/examples/basic-auth-with-router/README.md +60 -0
- jac_client/examples/basic-auth-with-router/app.jac +464 -0
- jac_client/examples/basic-auth-with-router/package.json +28 -0
- jac_client/examples/basic-auth-with-router/vite.config.js +28 -0
- jac_client/examples/basic-full-stack/.babelrc +9 -0
- jac_client/examples/basic-full-stack/README.md +18 -0
- jac_client/examples/basic-full-stack/app.jac +320 -0
- jac_client/examples/basic-full-stack/package.json +28 -0
- jac_client/examples/basic-full-stack/vite.config.js +28 -0
- jac_client/examples/full-stack-with-auth/.babelrc +9 -0
- jac_client/examples/full-stack-with-auth/README.md +16 -0
- jac_client/examples/full-stack-with-auth/app.jac +735 -0
- jac_client/examples/full-stack-with-auth/package.json +28 -0
- jac_client/examples/full-stack-with-auth/vite.config.js +30 -0
- jac_client/examples/little-x/app.jac +615 -0
- jac_client/examples/little-x/package.json +23 -0
- jac_client/examples/little-x/submit-button.jac +8 -0
- jac_client/examples/with-router/.babelrc +9 -0
- jac_client/examples/with-router/README.md +17 -0
- jac_client/examples/with-router/app.jac +323 -0
- jac_client/examples/with-router/package.json +28 -0
- jac_client/examples/with-router/vite.config.js +28 -0
- jac_client/plugin/cli.py +239 -0
- jac_client/plugin/client.py +89 -0
- jac_client/plugin/client_runtime.jac +234 -0
- jac_client/plugin/vite_client_bundle.py +355 -0
- jac_client/tests/__init__.py +2 -0
- jac_client/tests/fixtures/basic-app/app.jac +18 -0
- jac_client/tests/fixtures/client_app_with_antd/app.jac +28 -0
- jac_client/tests/fixtures/js_import/app.jac +30 -0
- jac_client/tests/fixtures/js_import/utils.js +22 -0
- jac_client/tests/fixtures/package-lock.json +329 -0
- jac_client/tests/fixtures/package.json +11 -0
- jac_client/tests/fixtures/relative_import/app.jac +13 -0
- jac_client/tests/fixtures/relative_import/button.jac +6 -0
- jac_client/tests/fixtures/spawn_test/app.jac +133 -0
- jac_client/tests/fixtures/test_fragments_spread/app.jac +53 -0
- jac_client/tests/test_cl.py +476 -0
- jac_client/tests/test_create_jac_app.py +139 -0
- jac_client-0.2.0.dist-info/METADATA +182 -0
- jac_client-0.2.0.dist-info/RECORD +72 -0
- jac_client-0.2.0.dist-info/WHEEL +4 -0
- jac_client-0.2.0.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
# Advanced State Management in Jac
|
|
2
|
+
|
|
3
|
+
Learn how to manage complex state in Jac using React hooks, combining multiple state instances, and building scalable state architectures.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📚 Table of Contents
|
|
8
|
+
|
|
9
|
+
- [React Hooks Overview](#react-hooks-overview)
|
|
10
|
+
- [Multiple State Variables](#multiple-state-variables)
|
|
11
|
+
- [State Composition Patterns](#state-composition-patterns)
|
|
12
|
+
- [Derived State](#derived-state)
|
|
13
|
+
- [Advanced React Hooks](#advanced-react-hooks)
|
|
14
|
+
- [State Management Patterns](#state-management-patterns)
|
|
15
|
+
- [Best Practices](#best-practices)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## React Hooks Overview
|
|
20
|
+
|
|
21
|
+
Jac uses React hooks for all state management. The most common hooks are:
|
|
22
|
+
|
|
23
|
+
- **`useState`**: Manage component state
|
|
24
|
+
- **`useEffect`**: Handle side effects and lifecycle
|
|
25
|
+
- **`useReducer`**: Manage complex state logic
|
|
26
|
+
- **`useContext`**: Share state across components
|
|
27
|
+
- **`useMemo`**: Memoize expensive computations
|
|
28
|
+
- **`useCallback`**: Memoize callback functions
|
|
29
|
+
|
|
30
|
+
### Basic useState Example
|
|
31
|
+
|
|
32
|
+
```jac
|
|
33
|
+
cl import from react { useState, useEffect }
|
|
34
|
+
|
|
35
|
+
cl {
|
|
36
|
+
def TodoApp() -> any {
|
|
37
|
+
let [todos, setTodos] = useState([]);
|
|
38
|
+
let [filter, setFilter] = useState("all");
|
|
39
|
+
let [loading, setLoading] = useState(False);
|
|
40
|
+
|
|
41
|
+
useEffect(lambda -> None {
|
|
42
|
+
async def loadTodos() -> None {
|
|
43
|
+
setLoading(True);
|
|
44
|
+
result = root spawn read_todos();
|
|
45
|
+
setTodos(result.reports);
|
|
46
|
+
setLoading(False);
|
|
47
|
+
}
|
|
48
|
+
loadTodos();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
return <div>{/* your UI */}</div>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def app() -> any {
|
|
55
|
+
return <TodoApp />;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Multiple State Variables
|
|
63
|
+
|
|
64
|
+
### Separating State by Concern
|
|
65
|
+
|
|
66
|
+
Instead of putting everything in one state object, split state into multiple variables:
|
|
67
|
+
|
|
68
|
+
```jac
|
|
69
|
+
cl import from react { useState, useEffect }
|
|
70
|
+
|
|
71
|
+
cl {
|
|
72
|
+
def TodoApp() -> any {
|
|
73
|
+
# Separate state variables for different concerns
|
|
74
|
+
let [todos, setTodos] = useState([]);
|
|
75
|
+
let [filter, setFilter] = useState("all");
|
|
76
|
+
let [loading, setLoading] = useState(False);
|
|
77
|
+
let [error, setError] = useState(None);
|
|
78
|
+
|
|
79
|
+
useEffect(lambda -> None {
|
|
80
|
+
async def loadTodos() -> None {
|
|
81
|
+
setLoading(True);
|
|
82
|
+
setError(None);
|
|
83
|
+
try {
|
|
84
|
+
result = root spawn read_todos();
|
|
85
|
+
setTodos(result.reports);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
setError(err);
|
|
88
|
+
} finally {
|
|
89
|
+
setLoading(False);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
loadTodos();
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
if loading { return <div>Loading...</div>; }
|
|
96
|
+
if error { return <div>Error: {error}</div>; }
|
|
97
|
+
|
|
98
|
+
return <div>
|
|
99
|
+
{todos.map(lambda todo: any -> any {
|
|
100
|
+
return <TodoItem key={todo._jac_id} todo={todo} />;
|
|
101
|
+
})}
|
|
102
|
+
</div>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def app() -> any {
|
|
106
|
+
return <TodoApp />;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Benefits:**
|
|
112
|
+
- **Separation of Concerns**: Each state variable manages one aspect
|
|
113
|
+
- **Selective Updates**: Only components using specific state re-render
|
|
114
|
+
- **Type Safety**: Each variable has its own type
|
|
115
|
+
- **Clearer Code**: Easy to understand what each state represents
|
|
116
|
+
|
|
117
|
+
### When to Use Object State
|
|
118
|
+
|
|
119
|
+
Sometimes an object makes sense for closely related data:
|
|
120
|
+
|
|
121
|
+
```jac
|
|
122
|
+
cl import from react { useState }
|
|
123
|
+
|
|
124
|
+
cl {
|
|
125
|
+
def UserProfile() -> any {
|
|
126
|
+
# Good: Related data in one object
|
|
127
|
+
let [user, setUser] = useState({
|
|
128
|
+
"name": "",
|
|
129
|
+
"email": "",
|
|
130
|
+
"avatar": ""
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
def updateName(name: str) -> None {
|
|
134
|
+
setUser({...user, "name": name});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return <div>
|
|
138
|
+
<input value={user.name} onChange={lambda e: any -> None {
|
|
139
|
+
updateName(e.target.value);
|
|
140
|
+
}} />
|
|
141
|
+
</div>;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## State Composition Patterns
|
|
149
|
+
|
|
150
|
+
### Pattern 1: Feature-Based State
|
|
151
|
+
|
|
152
|
+
Organize state by feature or domain using multiple `useState` calls:
|
|
153
|
+
|
|
154
|
+
```jac
|
|
155
|
+
cl import from react { useState, useEffect }
|
|
156
|
+
|
|
157
|
+
cl {
|
|
158
|
+
def App() -> any {
|
|
159
|
+
# User state
|
|
160
|
+
let [user, setUser] = useState(None);
|
|
161
|
+
let [isLoggedIn, setIsLoggedIn] = useState(False);
|
|
162
|
+
|
|
163
|
+
# Todo state
|
|
164
|
+
let [todos, setTodos] = useState([]);
|
|
165
|
+
let [selectedId, setSelectedId] = useState(None);
|
|
166
|
+
|
|
167
|
+
# UI state
|
|
168
|
+
let [theme, setTheme] = useState("light");
|
|
169
|
+
let [sidebarOpen, setSidebarOpen] = useState(False);
|
|
170
|
+
let [modalOpen, setModalOpen] = useState(False);
|
|
171
|
+
|
|
172
|
+
# Settings state
|
|
173
|
+
let [notifications, setNotifications] = useState(True);
|
|
174
|
+
let [language, setLanguage] = useState("en");
|
|
175
|
+
|
|
176
|
+
useEffect(lambda -> None {
|
|
177
|
+
async def loadData() -> None {
|
|
178
|
+
result = root spawn get_user_data();
|
|
179
|
+
setUser(result.user);
|
|
180
|
+
setIsLoggedIn(True);
|
|
181
|
+
}
|
|
182
|
+
loadData();
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
return <div className={theme}>
|
|
186
|
+
{sidebarOpen and <Sidebar />}
|
|
187
|
+
{todos.length > 0 and <TodoList items={todos} />}
|
|
188
|
+
</div>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def app() -> any {
|
|
192
|
+
return <App />;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Pattern 2: Local vs Global State
|
|
198
|
+
|
|
199
|
+
Use Context for global state and `useState` for local state:
|
|
200
|
+
|
|
201
|
+
```jac
|
|
202
|
+
cl import from react { useState, useContext, createContext }
|
|
203
|
+
|
|
204
|
+
cl {
|
|
205
|
+
# Create context for global state
|
|
206
|
+
AppContext = createContext(None);
|
|
207
|
+
|
|
208
|
+
def App() -> any {
|
|
209
|
+
# Global state
|
|
210
|
+
let [currentUser, setCurrentUser] = useState(None);
|
|
211
|
+
let [theme, setTheme] = useState("light");
|
|
212
|
+
|
|
213
|
+
appValue = {
|
|
214
|
+
"currentUser": currentUser,
|
|
215
|
+
"theme": theme,
|
|
216
|
+
"setCurrentUser": setCurrentUser,
|
|
217
|
+
"setTheme": setTheme
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return <AppContext.Provider value={appValue}>
|
|
221
|
+
<TodoForm />
|
|
222
|
+
<TodoList />
|
|
223
|
+
</AppContext.Provider>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# Component with local state
|
|
227
|
+
def TodoForm() -> any {
|
|
228
|
+
# Access global context
|
|
229
|
+
app = useContext(AppContext);
|
|
230
|
+
|
|
231
|
+
# Local component state
|
|
232
|
+
let [text, setText] = useState("");
|
|
233
|
+
let [valid, setValid] = useState(False);
|
|
234
|
+
|
|
235
|
+
def validate(value: str) -> None {
|
|
236
|
+
setValid(len(value.trim()) > 0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return <form>
|
|
240
|
+
<input
|
|
241
|
+
value={text}
|
|
242
|
+
onChange={lambda e: any -> None {
|
|
243
|
+
newText = e.target.value;
|
|
244
|
+
setText(newText);
|
|
245
|
+
validate(newText);
|
|
246
|
+
}}
|
|
247
|
+
style={{"background": ("#333" if app.theme == "dark" else "#fff")}}
|
|
248
|
+
/>
|
|
249
|
+
</form>;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
def TodoList() -> any {
|
|
253
|
+
# Local list state
|
|
254
|
+
let [sortBy, setSortBy] = useState("date");
|
|
255
|
+
let [order, setOrder] = useState("asc");
|
|
256
|
+
|
|
257
|
+
# Access global context
|
|
258
|
+
app = useContext(AppContext);
|
|
259
|
+
|
|
260
|
+
return <div>
|
|
261
|
+
<h2>Welcome, {app.currentUser.name if app.currentUser else "Guest"}</h2>
|
|
262
|
+
</div>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
def app() -> any {
|
|
266
|
+
return <App />;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Pattern 3: Custom Hooks (State Modules)
|
|
272
|
+
|
|
273
|
+
Create reusable custom hooks for shared logic:
|
|
274
|
+
|
|
275
|
+
```jac
|
|
276
|
+
cl import from react { useState, useEffect }
|
|
277
|
+
cl import from '@jac-client/utils' { jacLogout }
|
|
278
|
+
|
|
279
|
+
cl {
|
|
280
|
+
# Custom hook: User management
|
|
281
|
+
def useUser() -> dict {
|
|
282
|
+
let [user, setUser] = useState(None);
|
|
283
|
+
let [loading, setLoading] = useState(False);
|
|
284
|
+
let [error, setError] = useState(None);
|
|
285
|
+
|
|
286
|
+
async def loadUser() -> None {
|
|
287
|
+
setLoading(True);
|
|
288
|
+
setError(None);
|
|
289
|
+
try {
|
|
290
|
+
result = root spawn get_current_user();
|
|
291
|
+
setUser(result);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
setError(err);
|
|
294
|
+
} finally {
|
|
295
|
+
setLoading(False);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
def logout() -> None {
|
|
300
|
+
jacLogout();
|
|
301
|
+
setUser(None);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
useEffect(lambda -> None {
|
|
305
|
+
loadUser();
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
"user": user,
|
|
310
|
+
"loading": loading,
|
|
311
|
+
"error": error,
|
|
312
|
+
"logout": logout,
|
|
313
|
+
"reload": loadUser
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
# Custom hook: Todo management
|
|
318
|
+
def useTodos() -> dict {
|
|
319
|
+
let [todos, setTodos] = useState([]);
|
|
320
|
+
let [loading, setLoading] = useState(False);
|
|
321
|
+
|
|
322
|
+
async def loadTodos() -> None {
|
|
323
|
+
setLoading(True);
|
|
324
|
+
try {
|
|
325
|
+
result = root spawn read_todos();
|
|
326
|
+
setTodos(result.reports);
|
|
327
|
+
} finally {
|
|
328
|
+
setLoading(False);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async def addTodo(text: str) -> None {
|
|
333
|
+
response = root spawn create_todo(text=text);
|
|
334
|
+
new_todo = response.reports[0][0];
|
|
335
|
+
setTodos(todos.concat([new_todo]));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async def toggleTodo(id: str) -> None {
|
|
339
|
+
id spawn toggle_todo();
|
|
340
|
+
setTodos(todos.map(lambda todo: any -> any {
|
|
341
|
+
if todo._jac_id == id {
|
|
342
|
+
return {...todo, "done": not todo.done};
|
|
343
|
+
}
|
|
344
|
+
return todo;
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
useEffect(lambda -> None {
|
|
349
|
+
loadTodos();
|
|
350
|
+
}, []);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
"todos": todos,
|
|
354
|
+
"loading": loading,
|
|
355
|
+
"addTodo": addTodo,
|
|
356
|
+
"toggleTodo": toggleTodo,
|
|
357
|
+
"reload": loadTodos
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Using custom hooks in components
|
|
362
|
+
def TodoApp() -> any {
|
|
363
|
+
userData = useUser();
|
|
364
|
+
todoData = useTodos();
|
|
365
|
+
|
|
366
|
+
if userData.loading or todoData.loading {
|
|
367
|
+
return <div>Loading...</div>;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return <div>
|
|
371
|
+
<h1>Welcome, {userData.user.name if userData.user else "Guest"}</h1>
|
|
372
|
+
<button onClick={userData.logout}>Logout</button>
|
|
373
|
+
{todoData.todos.map(lambda todo: any -> any {
|
|
374
|
+
return <TodoItem key={todo._jac_id} todo={todo} />;
|
|
375
|
+
})}
|
|
376
|
+
</div>;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
def app() -> any {
|
|
380
|
+
return <TodoApp />;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Derived State
|
|
388
|
+
|
|
389
|
+
### Computed Values with useMemo
|
|
390
|
+
|
|
391
|
+
Use `useMemo` to memoize expensive computations:
|
|
392
|
+
|
|
393
|
+
```jac
|
|
394
|
+
cl import from react { useState, useMemo }
|
|
395
|
+
|
|
396
|
+
cl {
|
|
397
|
+
def TodoApp() -> any {
|
|
398
|
+
let [todos, setTodos] = useState([]);
|
|
399
|
+
let [filter, setFilter] = useState("all");
|
|
400
|
+
|
|
401
|
+
# Memoized filtered todos - only recomputes when todos or filter changes
|
|
402
|
+
filteredTodos = useMemo(lambda -> list {
|
|
403
|
+
if filter == "active" {
|
|
404
|
+
return todos.filter(lambda item: any -> bool { return not item.done; });
|
|
405
|
+
} elif filter == "completed" {
|
|
406
|
+
return todos.filter(lambda item: any -> bool { return item.done; });
|
|
407
|
+
}
|
|
408
|
+
return todos;
|
|
409
|
+
}, [todos, filter]);
|
|
410
|
+
|
|
411
|
+
# Memoized stats - only recomputes when todos changes
|
|
412
|
+
stats = useMemo(lambda -> dict {
|
|
413
|
+
total = todos.length;
|
|
414
|
+
active = todos.filter(lambda item: any -> bool { return not item.done; }).length;
|
|
415
|
+
completed = total - active;
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
"total": total,
|
|
419
|
+
"active": active,
|
|
420
|
+
"completed": completed
|
|
421
|
+
};
|
|
422
|
+
}, [todos]);
|
|
423
|
+
|
|
424
|
+
return <div>
|
|
425
|
+
<div>
|
|
426
|
+
Total: {stats.total}, Active: {stats.active}, Completed: {stats.completed}
|
|
427
|
+
</div>
|
|
428
|
+
{filteredTodos.map(lambda item: any -> any {
|
|
429
|
+
return <TodoItem key={item._jac_id} todo={item} />;
|
|
430
|
+
})}
|
|
431
|
+
</div>;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
def app() -> any {
|
|
435
|
+
return <TodoApp />;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Simple Derived Values
|
|
441
|
+
|
|
442
|
+
For simple computations, you don't need `useMemo`:
|
|
443
|
+
|
|
444
|
+
```jac
|
|
445
|
+
cl import from react { useState }
|
|
446
|
+
|
|
447
|
+
cl {
|
|
448
|
+
def TodoApp() -> any {
|
|
449
|
+
let [todos, setTodos] = useState([]);
|
|
450
|
+
let [filter, setFilter] = useState("all");
|
|
451
|
+
|
|
452
|
+
# Simple derived values - computed on each render
|
|
453
|
+
def getFilteredTodos() -> list {
|
|
454
|
+
if filter == "active" {
|
|
455
|
+
return todos.filter(lambda item: any -> bool { return not item.done; });
|
|
456
|
+
} elif filter == "completed" {
|
|
457
|
+
return todos.filter(lambda item: any -> bool { return item.done; });
|
|
458
|
+
}
|
|
459
|
+
return todos;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
filtered = getFilteredTodos();
|
|
463
|
+
activeCount = todos.filter(lambda item: any -> bool { return not item.done; }).length;
|
|
464
|
+
|
|
465
|
+
return <div>
|
|
466
|
+
<div>{activeCount} active todos</div>
|
|
467
|
+
{filtered.map(lambda item: any -> any {
|
|
468
|
+
return <TodoItem key={item._jac_id} todo={item} />;
|
|
469
|
+
})}
|
|
470
|
+
</div>;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
def app() -> any {
|
|
474
|
+
return <TodoApp />;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Reactive Updates with useEffect
|
|
480
|
+
|
|
481
|
+
Use `useEffect` to sync derived state or perform side effects:
|
|
482
|
+
|
|
483
|
+
```jac
|
|
484
|
+
cl import from react { useState, useEffect }
|
|
485
|
+
|
|
486
|
+
cl {
|
|
487
|
+
def TodoApp() -> any {
|
|
488
|
+
let [todos, setTodos] = useState([]);
|
|
489
|
+
let [stats, setStats] = useState({
|
|
490
|
+
"total": 0,
|
|
491
|
+
"active": 0,
|
|
492
|
+
"completed": 0
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
# Update stats whenever todos change
|
|
496
|
+
useEffect(lambda -> None {
|
|
497
|
+
total = todos.length;
|
|
498
|
+
active = todos.filter(lambda item: any -> bool { return not item.done; }).length;
|
|
499
|
+
completed = total - active;
|
|
500
|
+
|
|
501
|
+
setStats({
|
|
502
|
+
"total": total,
|
|
503
|
+
"active": active,
|
|
504
|
+
"completed": completed
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
# Optional: Save to localStorage
|
|
508
|
+
localStorage.setItem("todoStats", JSON.stringify(stats));
|
|
509
|
+
}, [todos]);
|
|
510
|
+
|
|
511
|
+
return <div>
|
|
512
|
+
<StatsDisplay stats={stats} />
|
|
513
|
+
{todos.map(lambda item: any -> any {
|
|
514
|
+
return <TodoItem key={item._jac_id} todo={item} />;
|
|
515
|
+
})}
|
|
516
|
+
</div>;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
def app() -> any {
|
|
520
|
+
return <TodoApp />;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Advanced React Hooks
|
|
528
|
+
|
|
529
|
+
### useReducer for Complex State
|
|
530
|
+
|
|
531
|
+
When state logic becomes complex, use `useReducer` instead of `useState`:
|
|
532
|
+
|
|
533
|
+
```jac
|
|
534
|
+
cl import from react { useReducer, useEffect }
|
|
535
|
+
|
|
536
|
+
cl {
|
|
537
|
+
# Reducer function
|
|
538
|
+
def todoReducer(state: dict, action: dict) -> dict {
|
|
539
|
+
type = action.type;
|
|
540
|
+
|
|
541
|
+
if type == "ADD_TODO" {
|
|
542
|
+
return {...state, "todos": state.todos.concat([action.payload])};
|
|
543
|
+
} elif type == "TOGGLE_TODO" {
|
|
544
|
+
return {
|
|
545
|
+
...state,
|
|
546
|
+
"todos": state.todos.map(lambda todo: any -> any {
|
|
547
|
+
if todo._jac_id == action.payload {
|
|
548
|
+
return {...todo, "done": not todo.done};
|
|
549
|
+
}
|
|
550
|
+
return todo;
|
|
551
|
+
})
|
|
552
|
+
};
|
|
553
|
+
} elif type == "REMOVE_TODO" {
|
|
554
|
+
return {
|
|
555
|
+
...state,
|
|
556
|
+
"todos": state.todos.filter(lambda todo: any -> bool {
|
|
557
|
+
return todo._jac_id != action.payload;
|
|
558
|
+
})
|
|
559
|
+
};
|
|
560
|
+
} elif type == "SET_FILTER" {
|
|
561
|
+
return {...state, "filter": action.payload};
|
|
562
|
+
} elif type == "SET_LOADING" {
|
|
563
|
+
return {...state, "loading": action.payload};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return state;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
def TodoApp() -> any {
|
|
570
|
+
# Initial state
|
|
571
|
+
initialState = {
|
|
572
|
+
"todos": [],
|
|
573
|
+
"filter": "all",
|
|
574
|
+
"loading": False
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
let [state, dispatch] = useReducer(todoReducer, initialState);
|
|
578
|
+
|
|
579
|
+
useEffect(lambda -> None {
|
|
580
|
+
async def loadTodos() -> None {
|
|
581
|
+
dispatch({"type": "SET_LOADING", "payload": True});
|
|
582
|
+
result = root spawn read_todos();
|
|
583
|
+
for todo in result.reports {
|
|
584
|
+
dispatch({"type": "ADD_TODO", "payload": todo});
|
|
585
|
+
}
|
|
586
|
+
dispatch({"type": "SET_LOADING", "payload": False});
|
|
587
|
+
}
|
|
588
|
+
loadTodos();
|
|
589
|
+
}, []);
|
|
590
|
+
|
|
591
|
+
async def addTodo(text: str) -> None {
|
|
592
|
+
response = root spawn create_todo(text=text);
|
|
593
|
+
new_todo = response.reports[0][0];
|
|
594
|
+
dispatch({"type": "ADD_TODO", "payload": new_todo});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
def toggleTodo(id: str) -> None {
|
|
598
|
+
dispatch({"type": "TOGGLE_TODO", "payload": id});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return <div>
|
|
602
|
+
{state.loading and <div>Loading...</div>}
|
|
603
|
+
{state.todos.map(lambda todo: any -> any {
|
|
604
|
+
return <TodoItem
|
|
605
|
+
key={todo._jac_id}
|
|
606
|
+
todo={todo}
|
|
607
|
+
onToggle={lambda -> None { toggleTodo(todo._jac_id); }}
|
|
608
|
+
/>;
|
|
609
|
+
})}
|
|
610
|
+
</div>;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
def app() -> any {
|
|
614
|
+
return <TodoApp />;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### useContext for Global State
|
|
620
|
+
|
|
621
|
+
Share state across multiple components without prop drilling:
|
|
622
|
+
|
|
623
|
+
```jac
|
|
624
|
+
cl import from react { useState, useContext, createContext }
|
|
625
|
+
|
|
626
|
+
cl {
|
|
627
|
+
# Create context
|
|
628
|
+
TodoContext = createContext(None);
|
|
629
|
+
|
|
630
|
+
# Provider component
|
|
631
|
+
def TodoProvider(props: dict) -> any {
|
|
632
|
+
let [todos, setTodos] = useState([]);
|
|
633
|
+
let [filter, setFilter] = useState("all");
|
|
634
|
+
|
|
635
|
+
async def addTodo(text: str) -> None {
|
|
636
|
+
response = root spawn create_todo(text=text);
|
|
637
|
+
new_todo = response.reports[0][0];
|
|
638
|
+
setTodos(todos.concat([new_todo]));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async def toggleTodo(id: str) -> None {
|
|
642
|
+
id spawn toggle_todo();
|
|
643
|
+
setTodos(todos.map(lambda todo: any -> any {
|
|
644
|
+
if todo._jac_id == id {
|
|
645
|
+
return {...todo, "done": not todo.done};
|
|
646
|
+
}
|
|
647
|
+
return todo;
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
value = {
|
|
652
|
+
"todos": todos,
|
|
653
|
+
"filter": filter,
|
|
654
|
+
"setFilter": setFilter,
|
|
655
|
+
"addTodo": addTodo,
|
|
656
|
+
"toggleTodo": toggleTodo
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
return <TodoContext.Provider value={value}>
|
|
660
|
+
{props.children}
|
|
661
|
+
</TodoContext.Provider>;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# Hook to use context
|
|
665
|
+
def useTodoContext() -> dict {
|
|
666
|
+
return useContext(TodoContext);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
# Components using the context
|
|
670
|
+
def TodoList() -> any {
|
|
671
|
+
ctx = useTodoContext();
|
|
672
|
+
|
|
673
|
+
filteredTodos = ctx.todos.filter(lambda todo: any -> bool {
|
|
674
|
+
if ctx.filter == "active" { return not todo.done; }
|
|
675
|
+
if ctx.filter == "completed" { return todo.done; }
|
|
676
|
+
return True;
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
return <div>
|
|
680
|
+
{filteredTodos.map(lambda todo: any -> any {
|
|
681
|
+
return <TodoItem
|
|
682
|
+
key={todo._jac_id}
|
|
683
|
+
todo={todo}
|
|
684
|
+
onToggle={lambda -> None { ctx.toggleTodo(todo._jac_id); }}
|
|
685
|
+
/>;
|
|
686
|
+
})}
|
|
687
|
+
</div>;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
def FilterButtons() -> any {
|
|
691
|
+
ctx = useTodoContext();
|
|
692
|
+
|
|
693
|
+
return <div>
|
|
694
|
+
<button onClick={lambda -> None { ctx.setFilter("all"); }}>All</button>
|
|
695
|
+
<button onClick={lambda -> None { ctx.setFilter("active"); }}>Active</button>
|
|
696
|
+
<button onClick={lambda -> None { ctx.setFilter("completed"); }}>Completed</button>
|
|
697
|
+
</div>;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
# App with provider
|
|
701
|
+
def MainApp() -> any {
|
|
702
|
+
return <TodoProvider>
|
|
703
|
+
<FilterButtons />
|
|
704
|
+
<TodoList />
|
|
705
|
+
</TodoProvider>;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
def app() -> any {
|
|
709
|
+
return <MainApp />;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### useCallback for Memoized Functions
|
|
715
|
+
|
|
716
|
+
Prevent unnecessary re-renders by memoizing callbacks:
|
|
717
|
+
|
|
718
|
+
```jac
|
|
719
|
+
cl import from react { useState, useCallback }
|
|
720
|
+
|
|
721
|
+
cl {
|
|
722
|
+
def TodoApp() -> any {
|
|
723
|
+
let [todos, setTodos] = useState([]);
|
|
724
|
+
|
|
725
|
+
# Memoized callback - only recreated if todos changes
|
|
726
|
+
handleToggle = useCallback(lambda id: str -> None {
|
|
727
|
+
setTodos(todos.map(lambda todo: any -> any {
|
|
728
|
+
if todo._jac_id == id {
|
|
729
|
+
return {...todo, "done": not todo.done};
|
|
730
|
+
}
|
|
731
|
+
return todo;
|
|
732
|
+
}));
|
|
733
|
+
}, [todos]);
|
|
734
|
+
|
|
735
|
+
return <div>
|
|
736
|
+
{todos.map(lambda todo: any -> any {
|
|
737
|
+
return <TodoItem
|
|
738
|
+
key={todo._jac_id}
|
|
739
|
+
todo={todo}
|
|
740
|
+
onToggle={handleToggle}
|
|
741
|
+
/>;
|
|
742
|
+
})}
|
|
743
|
+
</div>;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
def app() -> any {
|
|
747
|
+
return <TodoApp />;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
## State Management Patterns
|
|
755
|
+
|
|
756
|
+
### Pattern 1: Action Functions
|
|
757
|
+
|
|
758
|
+
Encapsulate state logic in reusable action functions:
|
|
759
|
+
|
|
760
|
+
```jac
|
|
761
|
+
cl import from react { useState }
|
|
762
|
+
|
|
763
|
+
cl {
|
|
764
|
+
def TodoApp() -> any {
|
|
765
|
+
let [todos, setTodos] = useState([]);
|
|
766
|
+
let [filter, setFilter] = useState("all");
|
|
767
|
+
|
|
768
|
+
# Action functions
|
|
769
|
+
async def addTodo(text: str) -> None {
|
|
770
|
+
if not text.trim() { return; }
|
|
771
|
+
response = root spawn create_todo(text=text);
|
|
772
|
+
new_todo = response.reports[0][0];
|
|
773
|
+
setTodos(todos.concat([new_todo]));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async def toggleTodo(id: str) -> None {
|
|
777
|
+
id spawn toggle_todo();
|
|
778
|
+
setTodos(todos.map(lambda todo: any -> any {
|
|
779
|
+
if todo._jac_id == id {
|
|
780
|
+
return {...todo, "done": not todo.done};
|
|
781
|
+
}
|
|
782
|
+
return todo;
|
|
783
|
+
}));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
def removeTodo(id: str) -> None {
|
|
787
|
+
setTodos(todos.filter(lambda todo: any -> bool {
|
|
788
|
+
return todo._jac_id != id;
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
def clearCompleted() -> None {
|
|
793
|
+
setTodos(todos.filter(lambda todo: any -> bool {
|
|
794
|
+
return not todo.done;
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return <div>
|
|
799
|
+
{/* UI using these actions */}
|
|
800
|
+
</div>;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
def app() -> any {
|
|
804
|
+
return <TodoApp />;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Pattern 2: Selector Functions with useMemo
|
|
810
|
+
|
|
811
|
+
Create memoized selector functions for derived data:
|
|
812
|
+
|
|
813
|
+
```jac
|
|
814
|
+
cl import from react { useState, useMemo }
|
|
815
|
+
|
|
816
|
+
cl {
|
|
817
|
+
def TodoApp() -> any {
|
|
818
|
+
let [todos, setTodos] = useState([]);
|
|
819
|
+
let [filter, setFilter] = useState("all");
|
|
820
|
+
|
|
821
|
+
# Memoized selectors
|
|
822
|
+
filteredTodos = useMemo(lambda -> list {
|
|
823
|
+
if filter == "active" {
|
|
824
|
+
return todos.filter(lambda t: any -> bool { return not t.done; });
|
|
825
|
+
} elif filter == "completed" {
|
|
826
|
+
return todos.filter(lambda t: any -> bool { return t.done; });
|
|
827
|
+
}
|
|
828
|
+
return todos;
|
|
829
|
+
}, [todos, filter]);
|
|
830
|
+
|
|
831
|
+
activeTodos = useMemo(lambda -> list {
|
|
832
|
+
return todos.filter(lambda t: any -> bool { return not t.done; });
|
|
833
|
+
}, [todos]);
|
|
834
|
+
|
|
835
|
+
completedTodos = useMemo(lambda -> list {
|
|
836
|
+
return todos.filter(lambda t: any -> bool { return t.done; });
|
|
837
|
+
}, [todos]);
|
|
838
|
+
|
|
839
|
+
return <div>
|
|
840
|
+
<div>Active: {activeTodos.length}</div>
|
|
841
|
+
<div>Completed: {completedTodos.length}</div>
|
|
842
|
+
{filteredTodos.map(lambda todo: any -> any {
|
|
843
|
+
return <TodoItem key={todo._jac_id} todo={todo} />;
|
|
844
|
+
})}
|
|
845
|
+
</div>;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
def app() -> any {
|
|
849
|
+
return <TodoApp />;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Pattern 3: Combining Multiple Hooks
|
|
855
|
+
|
|
856
|
+
Combine useState, useReducer, and useContext for complex applications:
|
|
857
|
+
|
|
858
|
+
```jac
|
|
859
|
+
cl import from react { useState, useReducer, useContext, createContext, useEffect }
|
|
860
|
+
|
|
861
|
+
cl {
|
|
862
|
+
# Context for global state
|
|
863
|
+
AppContext = createContext(None);
|
|
864
|
+
|
|
865
|
+
# Main app with combined hooks
|
|
866
|
+
def App() -> any {
|
|
867
|
+
# User state with useState
|
|
868
|
+
let [user, setUser] = useState(None);
|
|
869
|
+
|
|
870
|
+
# Todo state with useReducer
|
|
871
|
+
def todoReducer(state: dict, action: dict) -> dict {
|
|
872
|
+
if action.type == "ADD" {
|
|
873
|
+
return {...state, "todos": state.todos.concat([action.payload])};
|
|
874
|
+
} elif action.type == "TOGGLE" {
|
|
875
|
+
return {
|
|
876
|
+
...state,
|
|
877
|
+
"todos": state.todos.map(lambda t: any -> any {
|
|
878
|
+
if t._jac_id == action.payload {
|
|
879
|
+
return {...t, "done": not t.done};
|
|
880
|
+
}
|
|
881
|
+
return t;
|
|
882
|
+
})
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return state;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
let [todoState, dispatch] = useReducer(todoReducer, {"todos": [], "loading": False});
|
|
889
|
+
|
|
890
|
+
# UI state with useState
|
|
891
|
+
let [theme, setTheme] = useState("light");
|
|
892
|
+
|
|
893
|
+
useEffect(lambda -> None {
|
|
894
|
+
async def loadData() -> None {
|
|
895
|
+
userData = root spawn get_user();
|
|
896
|
+
setUser(userData);
|
|
897
|
+
}
|
|
898
|
+
loadData();
|
|
899
|
+
}, []);
|
|
900
|
+
|
|
901
|
+
contextValue = {
|
|
902
|
+
"user": user,
|
|
903
|
+
"setUser": setUser,
|
|
904
|
+
"todoState": todoState,
|
|
905
|
+
"dispatch": dispatch,
|
|
906
|
+
"theme": theme,
|
|
907
|
+
"setTheme": setTheme
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
return <AppContext.Provider value={contextValue}>
|
|
911
|
+
<TodoList />
|
|
912
|
+
</AppContext.Provider>;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
def app() -> any {
|
|
916
|
+
return <App />;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
---
|
|
922
|
+
|
|
923
|
+
## Complete Example: Full-Featured Todo App
|
|
924
|
+
|
|
925
|
+
Here's a complete example combining multiple React hooks and patterns:
|
|
926
|
+
|
|
927
|
+
```jac
|
|
928
|
+
cl import from react { useState, useEffect, useMemo, useCallback }
|
|
929
|
+
|
|
930
|
+
cl {
|
|
931
|
+
def TodoApp() -> any {
|
|
932
|
+
# State management
|
|
933
|
+
let [todos, setTodos] = useState([]);
|
|
934
|
+
let [filter, setFilter] = useState("all");
|
|
935
|
+
let [loading, setLoading] = useState(False);
|
|
936
|
+
let [error, setError] = useState(None);
|
|
937
|
+
let [user, setUser] = useState(None);
|
|
938
|
+
let [sidebarOpen, setSidebarOpen] = useState(False);
|
|
939
|
+
|
|
940
|
+
# Load initial data
|
|
941
|
+
useEffect(lambda -> None {
|
|
942
|
+
async def loadData() -> None {
|
|
943
|
+
setLoading(True);
|
|
944
|
+
setError(None);
|
|
945
|
+
try {
|
|
946
|
+
# Load user and todos in parallel
|
|
947
|
+
results = await Promise.all([
|
|
948
|
+
root spawn get_current_user(),
|
|
949
|
+
root spawn read_todos()
|
|
950
|
+
]);
|
|
951
|
+
setUser(results[0]);
|
|
952
|
+
setTodos(results[1].reports);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
setError(err);
|
|
955
|
+
} finally {
|
|
956
|
+
setLoading(False);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
loadData();
|
|
960
|
+
}, []);
|
|
961
|
+
|
|
962
|
+
# Memoized derived state
|
|
963
|
+
filteredTodos = useMemo(lambda -> list {
|
|
964
|
+
if filter == "active" {
|
|
965
|
+
return todos.filter(lambda t: any -> bool { return not t.done; });
|
|
966
|
+
} elif filter == "completed" {
|
|
967
|
+
return todos.filter(lambda t: any -> bool { return t.done; });
|
|
968
|
+
}
|
|
969
|
+
return todos;
|
|
970
|
+
}, [todos, filter]);
|
|
971
|
+
|
|
972
|
+
stats = useMemo(lambda -> dict {
|
|
973
|
+
total = todos.length;
|
|
974
|
+
active = todos.filter(lambda t: any -> bool { return not t.done; }).length;
|
|
975
|
+
return {"total": total, "active": active, "completed": total - active};
|
|
976
|
+
}, [todos]);
|
|
977
|
+
|
|
978
|
+
# Memoized action functions
|
|
979
|
+
addTodo = useCallback(lambda text: str -> None {
|
|
980
|
+
async def _addTodo() -> None {
|
|
981
|
+
response = root spawn create_todo(text=text);
|
|
982
|
+
new_todo = response.reports[0][0];
|
|
983
|
+
setTodos(todos.concat([new_todo]));
|
|
984
|
+
}
|
|
985
|
+
_addTodo();
|
|
986
|
+
}, [todos]);
|
|
987
|
+
|
|
988
|
+
toggleTodo = useCallback(lambda id: str -> None {
|
|
989
|
+
async def _toggleTodo() -> None {
|
|
990
|
+
id spawn toggle_todo();
|
|
991
|
+
setTodos(todos.map(lambda t: any -> any {
|
|
992
|
+
if t._jac_id == id {
|
|
993
|
+
return {...t, "done": not t.done};
|
|
994
|
+
}
|
|
995
|
+
return t;
|
|
996
|
+
}));
|
|
997
|
+
}
|
|
998
|
+
_toggleTodo();
|
|
999
|
+
}, [todos]);
|
|
1000
|
+
|
|
1001
|
+
removeTodo = useCallback(lambda id: str -> None {
|
|
1002
|
+
setTodos(todos.filter(lambda t: any -> bool { return t._jac_id != id; }));
|
|
1003
|
+
}, [todos]);
|
|
1004
|
+
|
|
1005
|
+
clearCompleted = useCallback(lambda -> None {
|
|
1006
|
+
setTodos(todos.filter(lambda t: any -> bool { return not t.done; }));
|
|
1007
|
+
}, [todos]);
|
|
1008
|
+
|
|
1009
|
+
toggleSidebar = useCallback(lambda -> None {
|
|
1010
|
+
setSidebarOpen(not sidebarOpen);
|
|
1011
|
+
}, [sidebarOpen]);
|
|
1012
|
+
|
|
1013
|
+
# Render
|
|
1014
|
+
if loading {
|
|
1015
|
+
return <div style={{"padding": "20px"}}>Loading...</div>;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if error {
|
|
1019
|
+
return <div style={{"padding": "20px", "color": "red"}}>
|
|
1020
|
+
Error: {error}
|
|
1021
|
+
</div>;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return <div style={{"display": "flex", "minHeight": "100vh"}}>
|
|
1025
|
+
# Sidebar
|
|
1026
|
+
{sidebarOpen and <div style={{"width": "250px", "padding": "20px", "background": "#f5f5f5"}}>
|
|
1027
|
+
<h3>Filter</h3>
|
|
1028
|
+
<button onClick={lambda -> None { setFilter("all"); }}>All ({stats.total})</button>
|
|
1029
|
+
<button onClick={lambda -> None { setFilter("active"); }}>Active ({stats.active})</button>
|
|
1030
|
+
<button onClick={lambda -> None { setFilter("completed"); }}>Completed ({stats.completed})</button>
|
|
1031
|
+
</div>}
|
|
1032
|
+
|
|
1033
|
+
# Main content
|
|
1034
|
+
<div style={{"flex": "1", "padding": "20px"}}>
|
|
1035
|
+
# Header
|
|
1036
|
+
<div style={{"display": "flex", "justifyContent": "space-between", "marginBottom": "20px"}}>
|
|
1037
|
+
<h1>Welcome, {user.name if user else "Guest"}</h1>
|
|
1038
|
+
<button onClick={toggleSidebar}>
|
|
1039
|
+
{"Hide" if sidebarOpen else "Show"} Sidebar
|
|
1040
|
+
</button>
|
|
1041
|
+
</div>
|
|
1042
|
+
|
|
1043
|
+
# Stats
|
|
1044
|
+
<div style={{"marginBottom": "20px"}}>
|
|
1045
|
+
{stats.active} active, {stats.completed} completed, {stats.total} total
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
1048
|
+
# Todo list
|
|
1049
|
+
<div>
|
|
1050
|
+
{filteredTodos.map(lambda todo: any -> any {
|
|
1051
|
+
return <div key={todo._jac_id} style={{"marginBottom": "10px"}}>
|
|
1052
|
+
<input
|
|
1053
|
+
type="checkbox"
|
|
1054
|
+
checked={todo.done}
|
|
1055
|
+
onChange={lambda -> None { toggleTodo(todo._jac_id); }}
|
|
1056
|
+
/>
|
|
1057
|
+
<span style={{"marginLeft": "10px"}}>{todo.text}</span>
|
|
1058
|
+
<button
|
|
1059
|
+
onClick={lambda -> None { removeTodo(todo._jac_id); }}
|
|
1060
|
+
style={{"marginLeft": "10px"}}
|
|
1061
|
+
>
|
|
1062
|
+
Delete
|
|
1063
|
+
</button>
|
|
1064
|
+
</div>;
|
|
1065
|
+
})}
|
|
1066
|
+
</div>
|
|
1067
|
+
|
|
1068
|
+
# Clear completed button
|
|
1069
|
+
{stats.completed > 0 and <button
|
|
1070
|
+
onClick={clearCompleted}
|
|
1071
|
+
style={{"marginTop": "20px"}}
|
|
1072
|
+
>
|
|
1073
|
+
Clear Completed
|
|
1074
|
+
</button>}
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
def app() -> any {
|
|
1080
|
+
return <TodoApp />;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
---
|
|
1086
|
+
|
|
1087
|
+
## Best Practices
|
|
1088
|
+
|
|
1089
|
+
### 1. Separate State Variables by Concern
|
|
1090
|
+
|
|
1091
|
+
```jac
|
|
1092
|
+
cl import from react { useState }
|
|
1093
|
+
|
|
1094
|
+
# ✅ Good: Separate state variables
|
|
1095
|
+
def App() -> any {
|
|
1096
|
+
let [user, setUser] = useState(None);
|
|
1097
|
+
let [todos, setTodos] = useState([]);
|
|
1098
|
+
let [sidebarOpen, setSidebarOpen] = useState(False);
|
|
1099
|
+
let [theme, setTheme] = useState("light");
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
# ❌ Avoid: One giant state object for unrelated data
|
|
1103
|
+
def App() -> any {
|
|
1104
|
+
let [appState, setAppState] = useState({
|
|
1105
|
+
"user": None,
|
|
1106
|
+
"todos": [],
|
|
1107
|
+
"sidebarOpen": False,
|
|
1108
|
+
"theme": "light"
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
### 2. Use useMemo for Expensive Computations
|
|
1114
|
+
|
|
1115
|
+
```jac
|
|
1116
|
+
cl import from react { useState, useMemo }
|
|
1117
|
+
|
|
1118
|
+
# ✅ Good: Memoize expensive calculations
|
|
1119
|
+
def TodoApp() -> any {
|
|
1120
|
+
let [todos, setTodos] = useState([]);
|
|
1121
|
+
|
|
1122
|
+
activeTodos = useMemo(lambda -> list {
|
|
1123
|
+
return todos.filter(lambda t: any -> bool { return not t.done; });
|
|
1124
|
+
}, [todos]);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
# ❌ Avoid: Computing on every render
|
|
1128
|
+
def TodoApp() -> any {
|
|
1129
|
+
let [todos, setTodos] = useState([]);
|
|
1130
|
+
|
|
1131
|
+
# This runs on every render, even if todos hasn't changed
|
|
1132
|
+
activeTodos = todos.filter(lambda t: any -> bool { return not t.done; });
|
|
1133
|
+
}
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
### 3. Don't Store Derived State
|
|
1137
|
+
|
|
1138
|
+
```jac
|
|
1139
|
+
cl import from react { useState }
|
|
1140
|
+
|
|
1141
|
+
# ✅ Good: Calculate derived values
|
|
1142
|
+
def TodoApp() -> any {
|
|
1143
|
+
let [todos, setTodos] = useState([]);
|
|
1144
|
+
activeCount = todos.filter(lambda t: any -> bool { return not t.done; }).length;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
# ❌ Avoid: Storing derived values in state
|
|
1148
|
+
def TodoApp() -> any {
|
|
1149
|
+
let [todos, setTodos] = useState([]);
|
|
1150
|
+
let [activeCount, setActiveCount] = useState(0); # Redundant!
|
|
1151
|
+
}
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
### 4. Use useReducer for Complex State Logic
|
|
1155
|
+
|
|
1156
|
+
```jac
|
|
1157
|
+
cl import from react { useReducer }
|
|
1158
|
+
|
|
1159
|
+
# ✅ Good: useReducer for complex interdependent state
|
|
1160
|
+
def TodoApp() -> any {
|
|
1161
|
+
def reducer(state: dict, action: dict) -> dict {
|
|
1162
|
+
if action.type == "ADD" {
|
|
1163
|
+
return {...state, "todos": state.todos.concat([action.payload]), "count": state.count + 1};
|
|
1164
|
+
}
|
|
1165
|
+
return state;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
let [state, dispatch] = useReducer(reducer, {"todos": [], "count": 0});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
# ❌ Avoid: Multiple useState for interdependent state
|
|
1172
|
+
def TodoApp() -> any {
|
|
1173
|
+
let [todos, setTodos] = useState([]);
|
|
1174
|
+
let [count, setCount] = useState(0);
|
|
1175
|
+
# Risk of inconsistency - need to update both together
|
|
1176
|
+
}
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
### 5. Always Handle Loading and Error States
|
|
1180
|
+
|
|
1181
|
+
```jac
|
|
1182
|
+
cl import from react { useState, useEffect }
|
|
1183
|
+
|
|
1184
|
+
# ✅ Good: Comprehensive state management
|
|
1185
|
+
def TodoApp() -> any {
|
|
1186
|
+
let [todos, setTodos] = useState([]);
|
|
1187
|
+
let [loading, setLoading] = useState(False);
|
|
1188
|
+
let [error, setError] = useState(None);
|
|
1189
|
+
|
|
1190
|
+
useEffect(lambda -> None {
|
|
1191
|
+
async def loadTodos() -> None {
|
|
1192
|
+
setLoading(True);
|
|
1193
|
+
setError(None);
|
|
1194
|
+
try {
|
|
1195
|
+
result = root spawn read_todos();
|
|
1196
|
+
setTodos(result.reports);
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
setError(err);
|
|
1199
|
+
} finally {
|
|
1200
|
+
setLoading(False);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
loadTodos();
|
|
1204
|
+
}, []);
|
|
1205
|
+
|
|
1206
|
+
if loading { return <div>Loading...</div>; }
|
|
1207
|
+
if error { return <div>Error: {error}</div>; }
|
|
1208
|
+
return <div>{/* todos */}</div>;
|
|
1209
|
+
}
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
### 6. Use useCallback for Callbacks Passed to Children
|
|
1213
|
+
|
|
1214
|
+
```jac
|
|
1215
|
+
cl import from react { useState, useCallback }
|
|
1216
|
+
|
|
1217
|
+
# ✅ Good: Memoized callbacks prevent unnecessary re-renders
|
|
1218
|
+
def TodoApp() -> any {
|
|
1219
|
+
let [todos, setTodos] = useState([]);
|
|
1220
|
+
|
|
1221
|
+
handleToggle = useCallback(lambda id: str -> None {
|
|
1222
|
+
setTodos(todos.map(lambda t: any -> any {
|
|
1223
|
+
if t._jac_id == id { return {...t, "done": not t.done}; }
|
|
1224
|
+
return t;
|
|
1225
|
+
}));
|
|
1226
|
+
}, [todos]);
|
|
1227
|
+
|
|
1228
|
+
return <TodoList todos={todos} onToggle={handleToggle} />;
|
|
1229
|
+
}
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### 7. Use Context for Deeply Nested Props
|
|
1233
|
+
|
|
1234
|
+
```jac
|
|
1235
|
+
cl import from react { useState, useContext, createContext }
|
|
1236
|
+
|
|
1237
|
+
# ✅ Good: Context avoids prop drilling
|
|
1238
|
+
ThemeContext = createContext("light");
|
|
1239
|
+
|
|
1240
|
+
def App() -> any {
|
|
1241
|
+
let [theme, setTheme] = useState("light");
|
|
1242
|
+
return <ThemeContext.Provider value={theme}>
|
|
1243
|
+
<DeeplyNestedComponent />
|
|
1244
|
+
</ThemeContext.Provider>;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
def DeeplyNestedComponent() -> any {
|
|
1248
|
+
theme = useContext(ThemeContext);
|
|
1249
|
+
return <div style={{"background": theme}}></div>;
|
|
1250
|
+
}
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
---
|
|
1254
|
+
|
|
1255
|
+
## Summary
|
|
1256
|
+
|
|
1257
|
+
- **useState**: Use for simple, independent state variables
|
|
1258
|
+
- **useReducer**: Use for complex, interdependent state logic
|
|
1259
|
+
- **useContext**: Use for global state and avoiding prop drilling
|
|
1260
|
+
- **useMemo**: Use to memoize expensive computations
|
|
1261
|
+
- **useCallback**: Use to memoize callbacks passed to child components
|
|
1262
|
+
- **Custom Hooks**: Create reusable state logic
|
|
1263
|
+
- **Best Practices**: Separate concerns, avoid derived state, handle errors
|
|
1264
|
+
|
|
1265
|
+
React hooks provide a powerful and flexible way to manage state in Jac applications! 🚀
|
|
1266
|
+
|