jac-client 0.1.0__py3-none-any.whl → 0.2.1__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 +232 -172
- jac_client/docs/advanced-state.md +1012 -452
- jac_client/docs/asset-serving/intro.md +209 -0
- jac_client/docs/assets/pipe_line-v2.svg +32 -0
- jac_client/docs/assets/pipe_line.png +0 -0
- jac_client/docs/file-system/intro.md +90 -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 +538 -46
- jac_client/docs/lifecycle-hooks.md +517 -297
- jac_client/docs/routing.md +487 -357
- jac_client/docs/styling/intro.md +250 -0
- jac_client/docs/styling/js-styling.md +373 -0
- jac_client/docs/styling/material-ui.md +346 -0
- jac_client/docs/styling/pure-css.md +305 -0
- jac_client/docs/styling/sass.md +409 -0
- jac_client/docs/styling/styled-components.md +401 -0
- jac_client/docs/styling/tailwind.md +303 -0
- jac_client/examples/asset-serving/css-with-image/.babelrc +9 -0
- jac_client/examples/asset-serving/css-with-image/README.md +91 -0
- jac_client/examples/asset-serving/css-with-image/app.jac +67 -0
- jac_client/examples/asset-serving/css-with-image/assets/burger.png +0 -0
- jac_client/examples/asset-serving/css-with-image/package.json +28 -0
- jac_client/examples/asset-serving/css-with-image/styles.css +27 -0
- jac_client/examples/asset-serving/css-with-image/vite.config.js +29 -0
- jac_client/examples/asset-serving/image-asset/.babelrc +9 -0
- jac_client/examples/asset-serving/image-asset/README.md +119 -0
- jac_client/examples/asset-serving/image-asset/app.jac +43 -0
- jac_client/examples/asset-serving/image-asset/assets/burger.png +0 -0
- jac_client/examples/asset-serving/image-asset/package.json +28 -0
- jac_client/examples/asset-serving/image-asset/styles.css +27 -0
- jac_client/examples/asset-serving/image-asset/vite.config.js +29 -0
- jac_client/examples/asset-serving/import-alias/.babelrc +9 -0
- jac_client/examples/asset-serving/import-alias/README.md +83 -0
- jac_client/examples/asset-serving/import-alias/app.jac +57 -0
- jac_client/examples/asset-serving/import-alias/assets/burger.png +0 -0
- jac_client/examples/asset-serving/import-alias/package.json +28 -0
- jac_client/examples/asset-serving/import-alias/vite.config.js +29 -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/css-styling/js-styling/.babelrc +9 -0
- jac_client/examples/css-styling/js-styling/README.md +183 -0
- jac_client/examples/css-styling/js-styling/app.jac +63 -0
- jac_client/examples/css-styling/js-styling/package.json +28 -0
- jac_client/examples/css-styling/js-styling/styles.js +100 -0
- jac_client/examples/css-styling/js-styling/vite.config.js +28 -0
- jac_client/examples/css-styling/material-ui/.babelrc +9 -0
- jac_client/examples/css-styling/material-ui/README.md +16 -0
- jac_client/examples/css-styling/material-ui/app.jac +82 -0
- jac_client/examples/css-styling/material-ui/package.json +32 -0
- jac_client/examples/css-styling/material-ui/vite.config.js +28 -0
- jac_client/examples/css-styling/pure-css/.babelrc +9 -0
- jac_client/examples/css-styling/pure-css/README.md +16 -0
- jac_client/examples/css-styling/pure-css/app.jac +63 -0
- jac_client/examples/css-styling/pure-css/package.json +28 -0
- jac_client/examples/css-styling/pure-css/styles.css +112 -0
- jac_client/examples/css-styling/pure-css/vite.config.js +28 -0
- jac_client/examples/css-styling/sass-example/.babelrc +9 -0
- jac_client/examples/css-styling/sass-example/README.md +16 -0
- jac_client/examples/css-styling/sass-example/app.jac +63 -0
- jac_client/examples/css-styling/sass-example/package.json +29 -0
- jac_client/examples/css-styling/sass-example/styles.scss +158 -0
- jac_client/examples/css-styling/sass-example/vite.config.js +28 -0
- jac_client/examples/css-styling/styled-components/.babelrc +9 -0
- jac_client/examples/css-styling/styled-components/README.md +16 -0
- jac_client/examples/css-styling/styled-components/app.jac +66 -0
- jac_client/examples/css-styling/styled-components/package.json +29 -0
- jac_client/examples/css-styling/styled-components/styled.js +91 -0
- jac_client/examples/css-styling/styled-components/vite.config.js +28 -0
- jac_client/examples/css-styling/tailwind-example/.babelrc +9 -0
- jac_client/examples/css-styling/tailwind-example/README.md +16 -0
- jac_client/examples/css-styling/tailwind-example/app.jac +64 -0
- jac_client/examples/css-styling/tailwind-example/global.css +1 -0
- jac_client/examples/css-styling/tailwind-example/package.json +30 -0
- jac_client/examples/css-styling/tailwind-example/vite.config.js +30 -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/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 +95 -179
- jac_client/plugin/client.py +111 -2
- jac_client/plugin/client_runtime.jac +183 -890
- jac_client/plugin/vite_client_bundle.py +185 -205
- jac_client/tests/__init__.py +0 -1
- jac_client/tests/fixtures/{client_app.jac → basic-app/app.jac} +1 -1
- jac_client/tests/fixtures/cl_file/app.cl.jac +38 -0
- jac_client/tests/fixtures/cl_file/app.jac +15 -0
- jac_client/tests/fixtures/{client_app_with_antd.jac → client_app_with_antd/app.jac} +7 -0
- jac_client/tests/fixtures/{js_import.jac → js_import/app.jac} +2 -2
- jac_client/tests/fixtures/{relative_import.jac → relative_import/app.jac} +1 -1
- jac_client/tests/fixtures/{button.jac → relative_import/button.jac} +2 -2
- jac_client/tests/fixtures/spawn_test/app.jac +133 -0
- jac_client/tests/fixtures/{test_fragments_spread.jac → test_fragments_spread/app.jac} +11 -2
- jac_client/tests/test_asset_examples.py +339 -0
- jac_client/tests/test_cl.py +345 -151
- jac_client/tests/test_create_jac_app.py +41 -45
- {jac_client-0.1.0.dist-info → jac_client-0.2.1.dist-info}/METADATA +72 -16
- jac_client-0.2.1.dist-info/RECORD +140 -0
- jac_client/examples/little-x/package-lock.json +0 -2840
- jac_client/examples/todo-app/README.md +0 -82
- jac_client/examples/todo-app/app.jac +0 -683
- jac_client/examples/todo-app/package-lock.json +0 -999
- jac_client/examples/todo-app/package.json +0 -22
- jac_client-0.1.0.dist-info/RECORD +0 -33
- /jac_client/tests/fixtures/{utils.js → js_import/utils.js} +0 -0
- {jac_client-0.1.0.dist-info → jac_client-0.2.1.dist-info}/WHEEL +0 -0
- {jac_client-0.1.0.dist-info → jac_client-0.2.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,60 +1,147 @@
|
|
|
1
1
|
# Advanced State Management in Jac
|
|
2
2
|
|
|
3
|
-
Learn how to
|
|
3
|
+
Learn how to manage complex state in Jac using React hooks, combining multiple state instances, and building scalable state architectures.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## 📚 Table of Contents
|
|
8
8
|
|
|
9
|
-
- [
|
|
9
|
+
- [React Hooks Overview](#react-hooks-overview)
|
|
10
|
+
- [Multiple State Variables](#multiple-state-variables)
|
|
10
11
|
- [State Composition Patterns](#state-composition-patterns)
|
|
11
12
|
- [Derived State](#derived-state)
|
|
13
|
+
- [Advanced React Hooks](#advanced-react-hooks)
|
|
12
14
|
- [State Management Patterns](#state-management-patterns)
|
|
13
15
|
- [Best Practices](#best-practices)
|
|
14
16
|
|
|
15
17
|
---
|
|
16
18
|
|
|
17
|
-
##
|
|
19
|
+
## React Hooks Overview
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
Jac uses React hooks for all state management. The most common hooks are:
|
|
20
22
|
|
|
21
|
-
|
|
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
|
|
22
31
|
|
|
23
32
|
```jac
|
|
33
|
+
cl import from react { useState, useEffect }
|
|
34
|
+
|
|
24
35
|
cl {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
}, []);
|
|
29
50
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
51
|
+
return <div>{/* your UI */}</div>;
|
|
52
|
+
}
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 }
|
|
38
70
|
|
|
71
|
+
cl {
|
|
39
72
|
def TodoApp() -> any {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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>; }
|
|
43
97
|
|
|
44
98
|
return <div>
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
99
|
+
{todos.map(lambda todo: any -> any {
|
|
100
|
+
return <TodoItem key={todo._jac_id} todo={todo} />;
|
|
101
|
+
})}
|
|
48
102
|
</div>;
|
|
49
103
|
}
|
|
104
|
+
|
|
105
|
+
def app() -> any {
|
|
106
|
+
return <TodoApp />;
|
|
107
|
+
}
|
|
50
108
|
}
|
|
51
109
|
```
|
|
52
110
|
|
|
53
111
|
**Benefits:**
|
|
54
|
-
- **Separation of Concerns**: Each state manages one aspect
|
|
112
|
+
- **Separation of Concerns**: Each state variable manages one aspect
|
|
55
113
|
- **Selective Updates**: Only components using specific state re-render
|
|
56
|
-
- **
|
|
57
|
-
- **
|
|
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
|
+
```
|
|
58
145
|
|
|
59
146
|
---
|
|
60
147
|
|
|
@@ -62,151 +149,235 @@ cl {
|
|
|
62
149
|
|
|
63
150
|
### Pattern 1: Feature-Based State
|
|
64
151
|
|
|
65
|
-
Organize state by feature or domain:
|
|
152
|
+
Organize state by feature or domain using multiple `useState` calls:
|
|
66
153
|
|
|
67
154
|
```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
|
-
});
|
|
155
|
+
cl import from react { useState, useEffect }
|
|
93
156
|
|
|
157
|
+
cl {
|
|
94
158
|
def App() -> any {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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} />}
|
|
103
188
|
</div>;
|
|
104
189
|
}
|
|
190
|
+
|
|
191
|
+
def app() -> any {
|
|
192
|
+
return <App />;
|
|
193
|
+
}
|
|
105
194
|
}
|
|
106
195
|
```
|
|
107
196
|
|
|
108
197
|
### Pattern 2: Local vs Global State
|
|
109
198
|
|
|
110
|
-
Use
|
|
199
|
+
Use Context for global state and `useState` for local state:
|
|
111
200
|
|
|
112
201
|
```jac
|
|
202
|
+
cl import from react { useState, useContext, createContext }
|
|
203
|
+
|
|
113
204
|
cl {
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
225
|
|
|
120
|
-
# Component
|
|
226
|
+
# Component with local state
|
|
121
227
|
def TodoForm() -> any {
|
|
228
|
+
# Access global context
|
|
229
|
+
app = useContext(AppContext);
|
|
230
|
+
|
|
122
231
|
# Local component state
|
|
123
|
-
let [
|
|
124
|
-
|
|
125
|
-
"valid": False
|
|
126
|
-
});
|
|
232
|
+
let [text, setText] = useState("");
|
|
233
|
+
let [valid, setValid] = useState(False);
|
|
127
234
|
|
|
128
|
-
def validate() -> None {
|
|
129
|
-
|
|
130
|
-
setFormState({"valid": len(text.trim()) > 0});
|
|
235
|
+
def validate(value: str) -> None {
|
|
236
|
+
setValid(len(value.trim()) > 0);
|
|
131
237
|
}
|
|
132
238
|
|
|
133
239
|
return <form>
|
|
134
240
|
<input
|
|
135
|
-
value={
|
|
241
|
+
value={text}
|
|
136
242
|
onChange={lambda e: any -> None {
|
|
137
|
-
|
|
138
|
-
|
|
243
|
+
newText = e.target.value;
|
|
244
|
+
setText(newText);
|
|
245
|
+
validate(newText);
|
|
139
246
|
}}
|
|
247
|
+
style={{"background": ("#333" if app.theme == "dark" else "#fff")}}
|
|
140
248
|
/>
|
|
141
249
|
</form>;
|
|
142
250
|
}
|
|
143
251
|
|
|
144
252
|
def TodoList() -> any {
|
|
145
253
|
# Local list state
|
|
146
|
-
let [
|
|
147
|
-
|
|
148
|
-
"order": "asc"
|
|
149
|
-
});
|
|
254
|
+
let [sortBy, setSortBy] = useState("date");
|
|
255
|
+
let [order, setOrder] = useState("asc");
|
|
150
256
|
|
|
151
|
-
|
|
152
|
-
|
|
257
|
+
# Access global context
|
|
258
|
+
app = useContext(AppContext);
|
|
153
259
|
|
|
154
260
|
return <div>
|
|
155
|
-
{
|
|
261
|
+
<h2>Welcome, {app.currentUser.name if app.currentUser else "Guest"}</h2>
|
|
156
262
|
</div>;
|
|
157
263
|
}
|
|
264
|
+
|
|
265
|
+
def app() -> any {
|
|
266
|
+
return <App />;
|
|
267
|
+
}
|
|
158
268
|
}
|
|
159
269
|
```
|
|
160
270
|
|
|
161
|
-
### Pattern 3: State Modules
|
|
271
|
+
### Pattern 3: Custom Hooks (State Modules)
|
|
162
272
|
|
|
163
|
-
Create reusable
|
|
273
|
+
Create reusable custom hooks for shared logic:
|
|
164
274
|
|
|
165
275
|
```jac
|
|
276
|
+
cl import from react { useState, useEffect }
|
|
277
|
+
cl import from '@jac-client/utils' { jacLogout }
|
|
278
|
+
|
|
166
279
|
cl {
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
|
173
298
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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});
|
|
299
|
+
def logout() -> None {
|
|
300
|
+
jacLogout();
|
|
301
|
+
setUser(None);
|
|
181
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
|
+
};
|
|
182
315
|
}
|
|
183
316
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
};
|
|
187
359
|
}
|
|
188
360
|
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"loading": False
|
|
194
|
-
});
|
|
361
|
+
# Using custom hooks in components
|
|
362
|
+
def TodoApp() -> any {
|
|
363
|
+
userData = useUser();
|
|
364
|
+
todoData = useTodos();
|
|
195
365
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
todos = await __jacSpawn("read_todos");
|
|
200
|
-
setTodoState({"items": todos.reports, "loading": False});
|
|
201
|
-
} except Exception as err {
|
|
202
|
-
setTodoState({"loading": False});
|
|
366
|
+
if userData.loading or todoData.loading {
|
|
367
|
+
return <div>Loading...</div>;
|
|
203
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>;
|
|
204
377
|
}
|
|
205
378
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
s = todoState();
|
|
209
|
-
setTodoState({"items": s.items.concat([new_todo])});
|
|
379
|
+
def app() -> any {
|
|
380
|
+
return <TodoApp />;
|
|
210
381
|
}
|
|
211
382
|
}
|
|
212
383
|
```
|
|
@@ -215,389 +386,699 @@ cl {
|
|
|
215
386
|
|
|
216
387
|
## Derived State
|
|
217
388
|
|
|
218
|
-
### Computed Values
|
|
389
|
+
### Computed Values with useMemo
|
|
219
390
|
|
|
220
|
-
|
|
391
|
+
Use `useMemo` to memoize expensive computations:
|
|
221
392
|
|
|
222
393
|
```jac
|
|
223
|
-
cl {
|
|
224
|
-
let [todoState, setTodoState] = createState({
|
|
225
|
-
"items": []
|
|
226
|
-
});
|
|
394
|
+
cl import from react { useState, useMemo }
|
|
227
395
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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]);
|
|
231
423
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|
|
235
433
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
} elif filter == "completed" {
|
|
239
|
-
return [item for item in todos.items if item.done];
|
|
240
|
-
}
|
|
241
|
-
return todos.items;
|
|
434
|
+
def app() -> any {
|
|
435
|
+
return <TodoApp />;
|
|
242
436
|
}
|
|
437
|
+
}
|
|
438
|
+
```
|
|
243
439
|
|
|
244
|
-
|
|
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;
|
|
440
|
+
### Simple Derived Values
|
|
249
441
|
|
|
250
|
-
|
|
251
|
-
"total": total,
|
|
252
|
-
"active": active,
|
|
253
|
-
"completed": completed
|
|
254
|
-
};
|
|
255
|
-
}
|
|
442
|
+
For simple computations, you don't need `useMemo`:
|
|
256
443
|
|
|
444
|
+
```jac
|
|
445
|
+
cl import from react { useState }
|
|
446
|
+
|
|
447
|
+
cl {
|
|
257
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
|
+
|
|
258
462
|
filtered = getFilteredTodos();
|
|
259
|
-
|
|
463
|
+
activeCount = todos.filter(lambda item: any -> bool { return not item.done; }).length;
|
|
260
464
|
|
|
261
465
|
return <div>
|
|
262
|
-
<div>
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
466
|
+
<div>{activeCount} active todos</div>
|
|
467
|
+
{filtered.map(lambda item: any -> any {
|
|
468
|
+
return <TodoItem key={item._jac_id} todo={item} />;
|
|
469
|
+
})}
|
|
266
470
|
</div>;
|
|
267
471
|
}
|
|
472
|
+
|
|
473
|
+
def app() -> any {
|
|
474
|
+
return <TodoApp />;
|
|
475
|
+
}
|
|
268
476
|
}
|
|
269
477
|
```
|
|
270
478
|
|
|
271
|
-
### Reactive
|
|
479
|
+
### Reactive Updates with useEffect
|
|
272
480
|
|
|
273
|
-
Use `
|
|
481
|
+
Use `useEffect` to sync derived state or perform side effects:
|
|
274
482
|
|
|
275
483
|
```jac
|
|
484
|
+
cl import from react { useState, useEffect }
|
|
485
|
+
|
|
276
486
|
cl {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
487
|
+
def TodoApp() -> any {
|
|
488
|
+
let [todos, setTodos] = useState([]);
|
|
489
|
+
let [stats, setStats] = useState({
|
|
490
|
+
"total": 0,
|
|
491
|
+
"active": 0,
|
|
492
|
+
"completed": 0
|
|
493
|
+
});
|
|
280
494
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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;
|
|
286
500
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
completed = total - active;
|
|
293
|
-
|
|
294
|
-
setStatsState({
|
|
295
|
-
"total": total,
|
|
296
|
-
"active": active,
|
|
297
|
-
"completed": completed
|
|
298
|
-
});
|
|
299
|
-
});
|
|
501
|
+
setStats({
|
|
502
|
+
"total": total,
|
|
503
|
+
"active": active,
|
|
504
|
+
"completed": completed
|
|
505
|
+
});
|
|
300
506
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
507
|
+
# Optional: Save to localStorage
|
|
508
|
+
localStorage.setItem("todoStats", JSON.stringify(stats));
|
|
509
|
+
}, [todos]);
|
|
304
510
|
|
|
305
511
|
return <div>
|
|
306
512
|
<StatsDisplay stats={stats} />
|
|
307
|
-
{
|
|
513
|
+
{todos.map(lambda item: any -> any {
|
|
514
|
+
return <TodoItem key={item._jac_id} todo={item} />;
|
|
515
|
+
})}
|
|
308
516
|
</div>;
|
|
309
517
|
}
|
|
518
|
+
|
|
519
|
+
def app() -> any {
|
|
520
|
+
return <TodoApp />;
|
|
521
|
+
}
|
|
310
522
|
}
|
|
311
523
|
```
|
|
312
524
|
|
|
313
525
|
---
|
|
314
526
|
|
|
315
|
-
##
|
|
527
|
+
## Advanced React Hooks
|
|
316
528
|
|
|
317
|
-
###
|
|
529
|
+
### useReducer for Complex State
|
|
318
530
|
|
|
319
|
-
|
|
531
|
+
When state logic becomes complex, use `useReducer` instead of `useState`:
|
|
320
532
|
|
|
321
533
|
```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();
|
|
534
|
+
cl import from react { useReducer, useEffect }
|
|
331
535
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
})
|
|
337
559
|
};
|
|
338
|
-
|
|
560
|
+
} elif type == "SET_FILTER" {
|
|
561
|
+
return {...state, "filter": action.payload};
|
|
562
|
+
} elif type == "SET_LOADING" {
|
|
563
|
+
return {...state, "loading": action.payload};
|
|
564
|
+
}
|
|
339
565
|
|
|
340
|
-
|
|
341
|
-
|
|
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});
|
|
566
|
+
return state;
|
|
567
|
+
}
|
|
348
568
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
569
|
+
def TodoApp() -> any {
|
|
570
|
+
# Initial state
|
|
571
|
+
initialState = {
|
|
572
|
+
"todos": [],
|
|
573
|
+
"filter": "all",
|
|
574
|
+
"loading": False
|
|
575
|
+
};
|
|
352
576
|
|
|
353
|
-
|
|
354
|
-
setTodoState({"filter": payload.filter});
|
|
577
|
+
let [state, dispatch] = useReducer(todoReducer, initialState);
|
|
355
578
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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});
|
|
359
595
|
}
|
|
360
|
-
}
|
|
361
596
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
"id": new_todo._jac_id,
|
|
366
|
-
"text": new_todo.text
|
|
367
|
-
});
|
|
368
|
-
}
|
|
597
|
+
def toggleTodo(id: str) -> None {
|
|
598
|
+
dispatch({"type": "TOGGLE_TODO", "payload": id});
|
|
599
|
+
}
|
|
369
600
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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>;
|
|
373
611
|
}
|
|
374
612
|
|
|
375
|
-
def
|
|
376
|
-
|
|
613
|
+
def app() -> any {
|
|
614
|
+
return <TodoApp />;
|
|
377
615
|
}
|
|
378
616
|
}
|
|
379
617
|
```
|
|
380
618
|
|
|
381
|
-
###
|
|
619
|
+
### useContext for Global State
|
|
382
620
|
|
|
383
|
-
|
|
621
|
+
Share state across multiple components without prop drilling:
|
|
384
622
|
|
|
385
623
|
```jac
|
|
624
|
+
cl import from react { useState, useContext, createContext }
|
|
625
|
+
|
|
386
626
|
cl {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
}
|
|
393
650
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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>;
|
|
397
662
|
}
|
|
398
663
|
|
|
399
|
-
|
|
400
|
-
|
|
664
|
+
# Hook to use context
|
|
665
|
+
def useTodoContext() -> dict {
|
|
666
|
+
return useContext(TodoContext);
|
|
401
667
|
}
|
|
402
668
|
|
|
403
|
-
|
|
404
|
-
|
|
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>;
|
|
405
688
|
}
|
|
406
689
|
|
|
407
|
-
def
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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>;
|
|
415
698
|
}
|
|
416
699
|
|
|
417
|
-
|
|
418
|
-
|
|
700
|
+
# App with provider
|
|
701
|
+
def MainApp() -> any {
|
|
702
|
+
return <TodoProvider>
|
|
703
|
+
<FilterButtons />
|
|
704
|
+
<TodoList />
|
|
705
|
+
</TodoProvider>;
|
|
419
706
|
}
|
|
420
707
|
|
|
421
|
-
def
|
|
422
|
-
return
|
|
708
|
+
def app() -> any {
|
|
709
|
+
return <MainApp />;
|
|
423
710
|
}
|
|
424
711
|
}
|
|
425
712
|
```
|
|
426
713
|
|
|
427
|
-
###
|
|
714
|
+
### useCallback for Memoized Functions
|
|
428
715
|
|
|
429
|
-
|
|
716
|
+
Prevent unnecessary re-renders by memoizing callbacks:
|
|
430
717
|
|
|
431
718
|
```jac
|
|
719
|
+
cl import from react { useState, useCallback }
|
|
720
|
+
|
|
432
721
|
cl {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
"filter": "all"
|
|
436
|
-
});
|
|
722
|
+
def TodoApp() -> any {
|
|
723
|
+
let [todos, setTodos] = useState([]);
|
|
437
724
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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]);
|
|
443
734
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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>;
|
|
452
744
|
}
|
|
453
745
|
|
|
454
|
-
|
|
455
|
-
|
|
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});
|
|
746
|
+
def app() -> any {
|
|
747
|
+
return <TodoApp />;
|
|
464
748
|
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
465
751
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
## State Management Patterns
|
|
755
|
+
|
|
756
|
+
### Pattern 1: Action Functions
|
|
471
757
|
|
|
472
|
-
|
|
473
|
-
|
|
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>;
|
|
474
801
|
}
|
|
475
802
|
|
|
476
|
-
def
|
|
477
|
-
|
|
478
|
-
remaining = [item for item in s.items if not item.done];
|
|
479
|
-
setTodoState({"items": remaining});
|
|
803
|
+
def app() -> any {
|
|
804
|
+
return <TodoApp />;
|
|
480
805
|
}
|
|
481
806
|
}
|
|
482
807
|
```
|
|
483
808
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
## Complete Example: Multi-State Todo App
|
|
809
|
+
### Pattern 2: Selector Functions with useMemo
|
|
487
810
|
|
|
488
|
-
|
|
811
|
+
Create memoized selector functions for derived data:
|
|
489
812
|
|
|
490
813
|
```jac
|
|
814
|
+
cl import from react { useState, useMemo }
|
|
815
|
+
|
|
491
816
|
cl {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"
|
|
495
|
-
|
|
496
|
-
|
|
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]);
|
|
497
830
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"loading": False,
|
|
502
|
-
"error": None
|
|
503
|
-
});
|
|
831
|
+
activeTodos = useMemo(lambda -> list {
|
|
832
|
+
return todos.filter(lambda t: any -> bool { return not t.done; });
|
|
833
|
+
}, [todos]);
|
|
504
834
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
});
|
|
835
|
+
completedTodos = useMemo(lambda -> list {
|
|
836
|
+
return todos.filter(lambda t: any -> bool { return t.done; });
|
|
837
|
+
}, [todos]);
|
|
509
838
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
}
|
|
515
847
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
848
|
+
def app() -> any {
|
|
849
|
+
return <TodoApp />;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
```
|
|
520
853
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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;
|
|
551
886
|
}
|
|
552
|
-
}
|
|
553
887
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
561
908
|
};
|
|
562
|
-
setTodoState({"items": s.items.concat([newItem])});
|
|
563
|
-
}
|
|
564
909
|
|
|
565
|
-
|
|
566
|
-
|
|
910
|
+
return <AppContext.Provider value={contextValue}>
|
|
911
|
+
<TodoList />
|
|
912
|
+
</AppContext.Provider>;
|
|
567
913
|
}
|
|
568
914
|
|
|
569
|
-
def
|
|
570
|
-
|
|
571
|
-
setUiState({"sidebarOpen": not s.sidebarOpen});
|
|
915
|
+
def app() -> any {
|
|
916
|
+
return <App />;
|
|
572
917
|
}
|
|
918
|
+
}
|
|
919
|
+
```
|
|
573
920
|
|
|
574
|
-
|
|
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 {
|
|
575
931
|
def TodoApp() -> any {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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]);
|
|
579
1000
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1001
|
+
removeTodo = useCallback(lambda id: str -> None {
|
|
1002
|
+
setTodos(todos.filter(lambda t: any -> bool { return t._jac_id != id; }));
|
|
1003
|
+
}, [todos]);
|
|
583
1004
|
|
|
584
|
-
|
|
585
|
-
return
|
|
586
|
-
}
|
|
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]);
|
|
587
1012
|
|
|
588
|
-
|
|
589
|
-
|
|
1013
|
+
# Render
|
|
1014
|
+
if loading {
|
|
1015
|
+
return <div style={{"padding": "20px"}}>Loading...</div>;
|
|
590
1016
|
}
|
|
591
1017
|
|
|
592
|
-
|
|
593
|
-
|
|
1018
|
+
if error {
|
|
1019
|
+
return <div style={{"padding": "20px", "color": "red"}}>
|
|
1020
|
+
Error: {error}
|
|
1021
|
+
</div>;
|
|
1022
|
+
}
|
|
594
1023
|
|
|
595
|
-
return <div>
|
|
596
|
-
|
|
597
|
-
{
|
|
598
|
-
|
|
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>
|
|
599
1076
|
</div>;
|
|
600
1077
|
}
|
|
1078
|
+
|
|
1079
|
+
def app() -> any {
|
|
1080
|
+
return <TodoApp />;
|
|
1081
|
+
}
|
|
601
1082
|
}
|
|
602
1083
|
```
|
|
603
1084
|
|
|
@@ -605,102 +1086,181 @@ cl {
|
|
|
605
1086
|
|
|
606
1087
|
## Best Practices
|
|
607
1088
|
|
|
608
|
-
### 1. Separate
|
|
1089
|
+
### 1. Separate State Variables by Concern
|
|
609
1090
|
|
|
610
1091
|
```jac
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
let [
|
|
618
|
-
"
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
}
|
|
623
1111
|
```
|
|
624
1112
|
|
|
625
|
-
### 2.
|
|
1113
|
+
### 2. Use useMemo for Expensive Computations
|
|
626
1114
|
|
|
627
1115
|
```jac
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
});
|
|
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
|
+
}
|
|
645
1134
|
```
|
|
646
1135
|
|
|
647
|
-
### 3.
|
|
1136
|
+
### 3. Don't Store Derived State
|
|
648
1137
|
|
|
649
1138
|
```jac
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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;
|
|
653
1145
|
}
|
|
654
1146
|
|
|
655
|
-
# ❌ Avoid: Storing
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
1147
|
+
# ❌ Avoid: Storing derived values in state
|
|
1148
|
+
def TodoApp() -> any {
|
|
1149
|
+
let [todos, setTodos] = useState([]);
|
|
1150
|
+
let [activeCount, setActiveCount] = useState(0); # Redundant!
|
|
1151
|
+
}
|
|
660
1152
|
```
|
|
661
1153
|
|
|
662
|
-
### 4.
|
|
1154
|
+
### 4. Use useReducer for Complex State Logic
|
|
663
1155
|
|
|
664
1156
|
```jac
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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});
|
|
670
1169
|
}
|
|
671
1170
|
|
|
672
|
-
# ❌ Avoid:
|
|
673
|
-
def
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
676
1176
|
}
|
|
677
1177
|
```
|
|
678
1178
|
|
|
679
|
-
### 5. Handle Loading and Error States
|
|
1179
|
+
### 5. Always Handle Loading and Error States
|
|
680
1180
|
|
|
681
1181
|
```jac
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
+
}
|
|
693
1251
|
```
|
|
694
1252
|
|
|
695
1253
|
---
|
|
696
1254
|
|
|
697
1255
|
## Summary
|
|
698
1256
|
|
|
699
|
-
- **
|
|
700
|
-
- **
|
|
701
|
-
- **
|
|
702
|
-
- **
|
|
703
|
-
- **
|
|
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
|
|
704
1264
|
|
|
705
|
-
|
|
1265
|
+
React hooks provide a powerful and flexible way to manage state in Jac applications! 🚀
|
|
706
1266
|
|