autoworkflow 3.1.4 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Jotai Skill
|
|
2
|
+
|
|
3
|
+
## Primitive Atoms
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
|
|
6
|
+
|
|
7
|
+
// Primitive atom (read-write)
|
|
8
|
+
const countAtom = atom(0);
|
|
9
|
+
const userAtom = atom<User | null>(null);
|
|
10
|
+
const isDarkModeAtom = atom(false);
|
|
11
|
+
|
|
12
|
+
// Usage in components
|
|
13
|
+
function Counter() {
|
|
14
|
+
// Read and write
|
|
15
|
+
const [count, setCount] = useAtom(countAtom);
|
|
16
|
+
|
|
17
|
+
// Read only
|
|
18
|
+
const count = useAtomValue(countAtom);
|
|
19
|
+
|
|
20
|
+
// Write only (doesn't subscribe to changes)
|
|
21
|
+
const setCount = useSetAtom(countAtom);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div>
|
|
25
|
+
<span>{count}</span>
|
|
26
|
+
<button onClick={() => setCount((c) => c + 1)}>+</button>
|
|
27
|
+
<button onClick={() => setCount(0)}>Reset</button>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
## Derived Atoms (Computed Values)
|
|
34
|
+
\`\`\`typescript
|
|
35
|
+
// Read-only derived atom
|
|
36
|
+
const userNameAtom = atom((get) => get(userAtom)?.name ?? 'Guest');
|
|
37
|
+
|
|
38
|
+
const isLoggedInAtom = atom((get) => !!get(userAtom));
|
|
39
|
+
|
|
40
|
+
// Derived from multiple atoms
|
|
41
|
+
const totalAtom = atom((get) => {
|
|
42
|
+
const items = get(cartItemsAtom);
|
|
43
|
+
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Derived with async
|
|
47
|
+
const userPostsAtom = atom(async (get) => {
|
|
48
|
+
const user = get(userAtom);
|
|
49
|
+
if (!user) return [];
|
|
50
|
+
|
|
51
|
+
const response = await fetch(\`/api/users/\${user.id}/posts\`);
|
|
52
|
+
return response.json();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Usage
|
|
56
|
+
function UserPosts() {
|
|
57
|
+
const posts = useAtomValue(userPostsAtom); // Suspense required
|
|
58
|
+
return <PostList posts={posts} />;
|
|
59
|
+
}
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
## Writable Derived Atoms
|
|
63
|
+
\`\`\`typescript
|
|
64
|
+
// Read-write derived atom
|
|
65
|
+
const uppercaseNameAtom = atom(
|
|
66
|
+
// Getter
|
|
67
|
+
(get) => get(nameAtom).toUpperCase(),
|
|
68
|
+
// Setter
|
|
69
|
+
(get, set, newName: string) => {
|
|
70
|
+
set(nameAtom, newName.toLowerCase());
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Lens-like pattern (update nested state)
|
|
75
|
+
const userNameAtom = atom(
|
|
76
|
+
(get) => get(userAtom)?.name ?? '',
|
|
77
|
+
(get, set, newName: string) => {
|
|
78
|
+
const user = get(userAtom);
|
|
79
|
+
if (user) {
|
|
80
|
+
set(userAtom, { ...user, name: newName });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Action atom (write-only)
|
|
86
|
+
const incrementAtom = atom(null, (get, set) => {
|
|
87
|
+
set(countAtom, get(countAtom) + 1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const fetchUserAtom = atom(null, async (get, set, userId: string) => {
|
|
91
|
+
set(loadingAtom, true);
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(\`/api/users/\${userId}\`);
|
|
94
|
+
const user = await response.json();
|
|
95
|
+
set(userAtom, user);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
set(errorAtom, (error as Error).message);
|
|
98
|
+
} finally {
|
|
99
|
+
set(loadingAtom, false);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Usage
|
|
104
|
+
function UserLoader({ userId }: { userId: string }) {
|
|
105
|
+
const fetchUser = useSetAtom(fetchUserAtom);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
fetchUser(userId);
|
|
109
|
+
}, [userId, fetchUser]);
|
|
110
|
+
|
|
111
|
+
return <UserProfile />;
|
|
112
|
+
}
|
|
113
|
+
\`\`\`
|
|
114
|
+
|
|
115
|
+
## Atom Families
|
|
116
|
+
\`\`\`typescript
|
|
117
|
+
import { atomFamily } from 'jotai/utils';
|
|
118
|
+
|
|
119
|
+
// Create atoms dynamically by parameter
|
|
120
|
+
const todoAtomFamily = atomFamily((id: string) =>
|
|
121
|
+
atom<Todo | null>(null)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const userByIdAtom = atomFamily((userId: string) =>
|
|
125
|
+
atom(async () => {
|
|
126
|
+
const response = await fetch(\`/api/users/\${userId}\`);
|
|
127
|
+
return response.json();
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Usage
|
|
132
|
+
function TodoItem({ id }: { id: string }) {
|
|
133
|
+
const [todo, setTodo] = useAtom(todoAtomFamily(id));
|
|
134
|
+
// ...
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function UserCard({ userId }: { userId: string }) {
|
|
138
|
+
const user = useAtomValue(userByIdAtom(userId));
|
|
139
|
+
// ...
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// With default value
|
|
143
|
+
const settingAtomFamily = atomFamily(
|
|
144
|
+
(key: string) => atom(localStorage.getItem(key) ?? ''),
|
|
145
|
+
(a, b) => a === b // Equality check
|
|
146
|
+
);
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## Persistence
|
|
150
|
+
\`\`\`typescript
|
|
151
|
+
import { atomWithStorage } from 'jotai/utils';
|
|
152
|
+
|
|
153
|
+
// Auto-sync with localStorage
|
|
154
|
+
const themeAtom = atomWithStorage('theme', 'light');
|
|
155
|
+
const userPrefsAtom = atomWithStorage<UserPrefs>('userPrefs', {
|
|
156
|
+
notifications: true,
|
|
157
|
+
language: 'en',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Custom storage (sessionStorage)
|
|
161
|
+
const sessionAtom = atomWithStorage('session', null, {
|
|
162
|
+
getItem: (key) => {
|
|
163
|
+
const value = sessionStorage.getItem(key);
|
|
164
|
+
return value ? JSON.parse(value) : null;
|
|
165
|
+
},
|
|
166
|
+
setItem: (key, value) => {
|
|
167
|
+
sessionStorage.setItem(key, JSON.stringify(value));
|
|
168
|
+
},
|
|
169
|
+
removeItem: (key) => {
|
|
170
|
+
sessionStorage.removeItem(key);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Async storage (e.g., IndexedDB)
|
|
175
|
+
import { createStore, get as idbGet, set as idbSet, del as idbDel } from 'idb-keyval';
|
|
176
|
+
|
|
177
|
+
const idbStorage = {
|
|
178
|
+
getItem: async (key: string) => await idbGet(key),
|
|
179
|
+
setItem: async (key: string, value: unknown) => await idbSet(key, value),
|
|
180
|
+
removeItem: async (key: string) => await idbDel(key),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const persistedAtom = atomWithStorage('key', defaultValue, idbStorage);
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
## Async Atoms & Suspense
|
|
187
|
+
\`\`\`typescript
|
|
188
|
+
import { atom, useAtomValue } from 'jotai';
|
|
189
|
+
import { Suspense } from 'react';
|
|
190
|
+
import { loadable } from 'jotai/utils';
|
|
191
|
+
|
|
192
|
+
// Async atom (requires Suspense)
|
|
193
|
+
const userAtom = atom(async () => {
|
|
194
|
+
const response = await fetch('/api/user');
|
|
195
|
+
return response.json();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// With Suspense
|
|
199
|
+
function App() {
|
|
200
|
+
return (
|
|
201
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
202
|
+
<UserProfile />
|
|
203
|
+
</Suspense>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function UserProfile() {
|
|
208
|
+
const user = useAtomValue(userAtom); // Suspends until resolved
|
|
209
|
+
return <div>{user.name}</div>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Without Suspense (using loadable)
|
|
213
|
+
const loadableUserAtom = loadable(userAtom);
|
|
214
|
+
|
|
215
|
+
function UserProfileNoSuspense() {
|
|
216
|
+
const userLoadable = useAtomValue(loadableUserAtom);
|
|
217
|
+
|
|
218
|
+
if (userLoadable.state === 'loading') return <div>Loading...</div>;
|
|
219
|
+
if (userLoadable.state === 'hasError') return <div>Error!</div>;
|
|
220
|
+
return <div>{userLoadable.data.name}</div>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Refresh async atom
|
|
224
|
+
import { useAtomRefresher } from 'jotai/utils';
|
|
225
|
+
|
|
226
|
+
function UserProfile() {
|
|
227
|
+
const user = useAtomValue(userAtom);
|
|
228
|
+
const refresh = useAtomRefresher(userAtom);
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div>
|
|
232
|
+
<span>{user.name}</span>
|
|
233
|
+
<button onClick={refresh}>Refresh</button>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
\`\`\`
|
|
238
|
+
|
|
239
|
+
## Reset & Utils
|
|
240
|
+
\`\`\`typescript
|
|
241
|
+
import { atomWithReset, useResetAtom, RESET } from 'jotai/utils';
|
|
242
|
+
|
|
243
|
+
// Resettable atom
|
|
244
|
+
const filterAtom = atomWithReset({
|
|
245
|
+
search: '',
|
|
246
|
+
category: 'all',
|
|
247
|
+
sortBy: 'date',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
function Filters() {
|
|
251
|
+
const [filter, setFilter] = useAtom(filterAtom);
|
|
252
|
+
const resetFilter = useResetAtom(filterAtom);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div>
|
|
256
|
+
<input
|
|
257
|
+
value={filter.search}
|
|
258
|
+
onChange={(e) => setFilter((f) => ({ ...f, search: e.target.value }))}
|
|
259
|
+
/>
|
|
260
|
+
<button onClick={resetFilter}>Reset Filters</button>
|
|
261
|
+
{/* Or: setFilter(RESET) */}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Select atom (derived with equality)
|
|
267
|
+
import { selectAtom } from 'jotai/utils';
|
|
268
|
+
|
|
269
|
+
const userNameAtom = selectAtom(userAtom, (user) => user?.name);
|
|
270
|
+
|
|
271
|
+
// Focus atom (lens for nested state)
|
|
272
|
+
import { focusAtom } from 'jotai-optics';
|
|
273
|
+
|
|
274
|
+
const nameAtom = focusAtom(userAtom, (optic) => optic.prop('name'));
|
|
275
|
+
\`\`\`
|
|
276
|
+
|
|
277
|
+
## Provider & Store
|
|
278
|
+
\`\`\`typescript
|
|
279
|
+
import { Provider, createStore, useStore } from 'jotai';
|
|
280
|
+
|
|
281
|
+
// Create isolated store
|
|
282
|
+
const myStore = createStore();
|
|
283
|
+
|
|
284
|
+
// Set initial values
|
|
285
|
+
myStore.set(countAtom, 10);
|
|
286
|
+
myStore.set(userAtom, { id: '1', name: 'John' });
|
|
287
|
+
|
|
288
|
+
// Get value outside React
|
|
289
|
+
const count = myStore.get(countAtom);
|
|
290
|
+
|
|
291
|
+
// Subscribe outside React
|
|
292
|
+
const unsubscribe = myStore.sub(countAtom, () => {
|
|
293
|
+
console.log('Count changed:', myStore.get(countAtom));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Provide store to React tree
|
|
297
|
+
function App() {
|
|
298
|
+
return (
|
|
299
|
+
<Provider store={myStore}>
|
|
300
|
+
<Counter />
|
|
301
|
+
</Provider>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Access store in component
|
|
306
|
+
function Debug() {
|
|
307
|
+
const store = useStore();
|
|
308
|
+
console.log('Store:', store.get(countAtom));
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
\`\`\`
|
|
312
|
+
|
|
313
|
+
## DevTools
|
|
314
|
+
\`\`\`typescript
|
|
315
|
+
import { useAtomsDebugValue } from 'jotai-devtools';
|
|
316
|
+
|
|
317
|
+
// In development
|
|
318
|
+
function DebugAtoms() {
|
|
319
|
+
useAtomsDebugValue();
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function App() {
|
|
324
|
+
return (
|
|
325
|
+
<Provider>
|
|
326
|
+
{process.env.NODE_ENV === 'development' && <DebugAtoms />}
|
|
327
|
+
<MainApp />
|
|
328
|
+
</Provider>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
\`\`\`
|
|
332
|
+
|
|
333
|
+
## ❌ DON'T
|
|
334
|
+
- Create atoms inside components (recreates on every render)
|
|
335
|
+
- Forget Suspense for async atoms
|
|
336
|
+
- Use useAtom when you only need read or write
|
|
337
|
+
- Mutate atom values directly
|
|
338
|
+
|
|
339
|
+
## ✅ DO
|
|
340
|
+
- Define atoms outside components (module level)
|
|
341
|
+
- Use useAtomValue for read-only subscriptions
|
|
342
|
+
- Use useSetAtom when you don't need the value
|
|
343
|
+
- Use atomFamily for dynamic/parameterized atoms
|
|
344
|
+
- Use atomWithStorage for persistence
|
|
345
|
+
- Use loadable for non-Suspense async handling
|
|
346
|
+
- Use derived atoms for computed values
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# MobX Skill
|
|
2
|
+
|
|
3
|
+
## Basic Store
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
import { makeAutoObservable, runInAction } from 'mobx';
|
|
6
|
+
|
|
7
|
+
interface User {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class UserStore {
|
|
14
|
+
// Observable state
|
|
15
|
+
user: User | null = null;
|
|
16
|
+
loading = false;
|
|
17
|
+
error: string | null = null;
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
// Automatically makes all properties observable,
|
|
21
|
+
// getters computed, and methods actions
|
|
22
|
+
makeAutoObservable(this);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Computed value (automatically cached)
|
|
26
|
+
get isLoggedIn() {
|
|
27
|
+
return !!this.user;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get displayName() {
|
|
31
|
+
return this.user?.name ?? 'Guest';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Synchronous action
|
|
35
|
+
setUser(user: User) {
|
|
36
|
+
this.user = user;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logout() {
|
|
40
|
+
this.user = null;
|
|
41
|
+
this.error = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Async action
|
|
45
|
+
async login(email: string, password: string) {
|
|
46
|
+
this.loading = true;
|
|
47
|
+
this.error = null;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch('/api/auth/login', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: JSON.stringify({ email, password }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!response.ok) throw new Error('Login failed');
|
|
56
|
+
|
|
57
|
+
const user = await response.json();
|
|
58
|
+
|
|
59
|
+
// Must wrap state changes after await in runInAction
|
|
60
|
+
runInAction(() => {
|
|
61
|
+
this.user = user;
|
|
62
|
+
this.loading = false;
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
runInAction(() => {
|
|
66
|
+
this.error = (error as Error).message;
|
|
67
|
+
this.loading = false;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const userStore = new UserStore();
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
## Manual Observable Configuration
|
|
77
|
+
\`\`\`typescript
|
|
78
|
+
import { makeObservable, observable, computed, action, flow } from 'mobx';
|
|
79
|
+
|
|
80
|
+
class TodoStore {
|
|
81
|
+
todos: Todo[] = [];
|
|
82
|
+
filter: 'all' | 'active' | 'completed' = 'all';
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
makeObservable(this, {
|
|
86
|
+
// State
|
|
87
|
+
todos: observable,
|
|
88
|
+
filter: observable,
|
|
89
|
+
// Computed
|
|
90
|
+
filteredTodos: computed,
|
|
91
|
+
totalCount: computed,
|
|
92
|
+
// Actions
|
|
93
|
+
addTodo: action,
|
|
94
|
+
toggleTodo: action,
|
|
95
|
+
removeTodo: action,
|
|
96
|
+
// Generators (for async)
|
|
97
|
+
fetchTodos: flow,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get filteredTodos() {
|
|
102
|
+
switch (this.filter) {
|
|
103
|
+
case 'active':
|
|
104
|
+
return this.todos.filter((t) => !t.completed);
|
|
105
|
+
case 'completed':
|
|
106
|
+
return this.todos.filter((t) => t.completed);
|
|
107
|
+
default:
|
|
108
|
+
return this.todos;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get totalCount() {
|
|
113
|
+
return this.todos.length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
addTodo(text: string) {
|
|
117
|
+
this.todos.push({
|
|
118
|
+
id: crypto.randomUUID(),
|
|
119
|
+
text,
|
|
120
|
+
completed: false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
toggleTodo(id: string) {
|
|
125
|
+
const todo = this.todos.find((t) => t.id === id);
|
|
126
|
+
if (todo) {
|
|
127
|
+
todo.completed = !todo.completed;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
removeTodo(id: string) {
|
|
132
|
+
this.todos = this.todos.filter((t) => t.id !== id);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Flow for async (alternative to runInAction)
|
|
136
|
+
*fetchTodos() {
|
|
137
|
+
this.loading = true;
|
|
138
|
+
try {
|
|
139
|
+
const response = yield fetch('/api/todos');
|
|
140
|
+
const todos = yield response.json();
|
|
141
|
+
this.todos = todos;
|
|
142
|
+
} finally {
|
|
143
|
+
this.loading = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## React Integration
|
|
150
|
+
\`\`\`tsx
|
|
151
|
+
import { observer } from 'mobx-react-lite';
|
|
152
|
+
import { userStore } from './stores/userStore';
|
|
153
|
+
|
|
154
|
+
// Wrap component with observer to react to observable changes
|
|
155
|
+
const UserProfile = observer(() => {
|
|
156
|
+
const { user, isLoggedIn, loading, logout } = userStore;
|
|
157
|
+
|
|
158
|
+
if (loading) return <div>Loading...</div>;
|
|
159
|
+
|
|
160
|
+
if (!isLoggedIn) {
|
|
161
|
+
return <button onClick={() => userStore.login('test@example.com', 'password')}>Login</button>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div>
|
|
166
|
+
<h1>Welcome, {user?.name}</h1>
|
|
167
|
+
<button onClick={logout}>Logout</button>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// With local observable state
|
|
173
|
+
import { useLocalObservable } from 'mobx-react-lite';
|
|
174
|
+
|
|
175
|
+
const Counter = observer(() => {
|
|
176
|
+
const state = useLocalObservable(() => ({
|
|
177
|
+
count: 0,
|
|
178
|
+
increment() {
|
|
179
|
+
this.count++;
|
|
180
|
+
},
|
|
181
|
+
decrement() {
|
|
182
|
+
this.count--;
|
|
183
|
+
},
|
|
184
|
+
get doubled() {
|
|
185
|
+
return this.count * 2;
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div>
|
|
191
|
+
<span>{state.count} (doubled: {state.doubled})</span>
|
|
192
|
+
<button onClick={state.increment}>+</button>
|
|
193
|
+
<button onClick={state.decrement}>-</button>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
\`\`\`
|
|
198
|
+
|
|
199
|
+
## Context Provider Pattern
|
|
200
|
+
\`\`\`tsx
|
|
201
|
+
import { createContext, useContext } from 'react';
|
|
202
|
+
import { UserStore } from './stores/UserStore';
|
|
203
|
+
import { TodoStore } from './stores/TodoStore';
|
|
204
|
+
|
|
205
|
+
// Root store combining all stores
|
|
206
|
+
class RootStore {
|
|
207
|
+
userStore: UserStore;
|
|
208
|
+
todoStore: TodoStore;
|
|
209
|
+
|
|
210
|
+
constructor() {
|
|
211
|
+
this.userStore = new UserStore(this);
|
|
212
|
+
this.todoStore = new TodoStore(this);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const StoreContext = createContext<RootStore | null>(null);
|
|
217
|
+
|
|
218
|
+
export function StoreProvider({ children }: { children: React.ReactNode }) {
|
|
219
|
+
const store = new RootStore();
|
|
220
|
+
return (
|
|
221
|
+
<StoreContext.Provider value={store}>
|
|
222
|
+
{children}
|
|
223
|
+
</StoreContext.Provider>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function useStore() {
|
|
228
|
+
const store = useContext(StoreContext);
|
|
229
|
+
if (!store) throw new Error('useStore must be used within StoreProvider');
|
|
230
|
+
return store;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Usage
|
|
234
|
+
const TodoList = observer(() => {
|
|
235
|
+
const { todoStore } = useStore();
|
|
236
|
+
return (
|
|
237
|
+
<ul>
|
|
238
|
+
{todoStore.filteredTodos.map((todo) => (
|
|
239
|
+
<li key={todo.id}>{todo.text}</li>
|
|
240
|
+
))}
|
|
241
|
+
</ul>
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
\`\`\`
|
|
245
|
+
|
|
246
|
+
## Reactions
|
|
247
|
+
\`\`\`typescript
|
|
248
|
+
import { autorun, reaction, when } from 'mobx';
|
|
249
|
+
|
|
250
|
+
// autorun - runs immediately and on every change
|
|
251
|
+
const dispose1 = autorun(() => {
|
|
252
|
+
console.log('User:', userStore.user?.name);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// reaction - only runs when specific data changes
|
|
256
|
+
const dispose2 = reaction(
|
|
257
|
+
() => userStore.user, // Data function
|
|
258
|
+
(user, previousUser) => { // Effect function
|
|
259
|
+
console.log('User changed from', previousUser, 'to', user);
|
|
260
|
+
if (user) {
|
|
261
|
+
localStorage.setItem('lastUserId', user.id);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{ fireImmediately: false } // Don't run on setup
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// when - runs once when condition is met
|
|
268
|
+
const dispose3 = when(
|
|
269
|
+
() => userStore.isLoggedIn,
|
|
270
|
+
() => {
|
|
271
|
+
console.log('User logged in!');
|
|
272
|
+
// Fetch user-specific data
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Cleanup
|
|
277
|
+
dispose1();
|
|
278
|
+
dispose2();
|
|
279
|
+
dispose3();
|
|
280
|
+
|
|
281
|
+
// In React useEffect
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
const dispose = autorun(() => {
|
|
284
|
+
document.title = \`\${todoStore.totalCount} todos\`;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return () => dispose();
|
|
288
|
+
}, []);
|
|
289
|
+
\`\`\`
|
|
290
|
+
|
|
291
|
+
## Persistence
|
|
292
|
+
\`\`\`typescript
|
|
293
|
+
import { makePersistable } from 'mobx-persist-store';
|
|
294
|
+
|
|
295
|
+
class SettingsStore {
|
|
296
|
+
theme: 'light' | 'dark' = 'light';
|
|
297
|
+
language = 'en';
|
|
298
|
+
|
|
299
|
+
constructor() {
|
|
300
|
+
makeAutoObservable(this);
|
|
301
|
+
|
|
302
|
+
makePersistable(this, {
|
|
303
|
+
name: 'SettingsStore',
|
|
304
|
+
properties: ['theme', 'language'],
|
|
305
|
+
storage: window.localStorage,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
setTheme(theme: 'light' | 'dark') {
|
|
310
|
+
this.theme = theme;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Or manual persistence with reaction
|
|
315
|
+
class UserStore {
|
|
316
|
+
constructor() {
|
|
317
|
+
makeAutoObservable(this);
|
|
318
|
+
|
|
319
|
+
// Load from storage
|
|
320
|
+
const stored = localStorage.getItem('user');
|
|
321
|
+
if (stored) {
|
|
322
|
+
this.user = JSON.parse(stored);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Save on changes
|
|
326
|
+
reaction(
|
|
327
|
+
() => this.user,
|
|
328
|
+
(user) => {
|
|
329
|
+
if (user) {
|
|
330
|
+
localStorage.setItem('user', JSON.stringify(user));
|
|
331
|
+
} else {
|
|
332
|
+
localStorage.removeItem('user');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
\`\`\`
|
|
339
|
+
|
|
340
|
+
## ❌ DON'T
|
|
341
|
+
- Forget to wrap components with observer
|
|
342
|
+
- Mutate state outside of actions
|
|
343
|
+
- Forget runInAction after await
|
|
344
|
+
- Destructure observables (loses reactivity)
|
|
345
|
+
- Create stores inside components
|
|
346
|
+
|
|
347
|
+
## ✅ DO
|
|
348
|
+
- Use makeAutoObservable for simple stores
|
|
349
|
+
- Use observer() for all components using observables
|
|
350
|
+
- Use runInAction or flow for async mutations
|
|
351
|
+
- Use reactions for side effects
|
|
352
|
+
- Access store properties directly (not destructured)
|
|
353
|
+
- Create stores once and share via context
|