opencode-effect-enforcer 0.2.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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,640 @@
1
+ ---
2
+ name: effect-atom-state
3
+ description: Implement reactive state management with Effect Atom for React applications
4
+ ---
5
+
6
+ # Effect Atom State Management
7
+
8
+ Effect Atom is a reactive state management library for Effect that seamlessly integrates with React.
9
+
10
+ ## Effect Source Reference
11
+
12
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
13
+ Browse and read files there directly to look up APIs, types, and implementations.
14
+
15
+ Reference this for:
16
+
17
+ - Atom reactivity: `packages/effect/src/unstable/reactivity/`
18
+ - AsyncResult source: `packages/effect/src/unstable/reactivity/AsyncResult.ts`
19
+ - Effect source: `packages/effect/src/`
20
+
21
+ ## Core Concepts
22
+
23
+ ### Atoms as References
24
+
25
+ Atoms work **by reference** - they are stable containers for reactive state:
26
+
27
+ ```typescript
28
+ import * as Atom from 'effect/unstable/reactivity/Atom';
29
+
30
+ // Atoms are created once and referenced throughout the app
31
+ export const counterAtom = Atom.make(0);
32
+
33
+ // Multiple components can reference the same atom
34
+ // All update when the atom value changes
35
+ ```
36
+
37
+ ### Automatic Cleanup
38
+
39
+ Atoms automatically reset when no subscribers remain (unless marked with `keepAlive`):
40
+
41
+ ```typescript
42
+ // Resets when last subscriber unmounts
43
+ export const temporaryState = Atom.make(initialValue);
44
+
45
+ // Persists across component lifecycles
46
+ export const persistentState = Atom.make(initialValue).pipe(Atom.keepAlive);
47
+ ```
48
+
49
+ ### Lazy Evaluation
50
+
51
+ Atom values are computed on-demand when subscribers access them.
52
+
53
+ ## Pattern: Basic Atoms
54
+
55
+ ```typescript
56
+ import * as Atom from 'effect/unstable/reactivity/Atom';
57
+
58
+ // Simple atom
59
+ export const count = Atom.make(0);
60
+
61
+ // Atom with object state
62
+ export interface CartState {
63
+ readonly items: ReadonlyArray<Item>;
64
+ readonly total: number;
65
+ }
66
+
67
+ export const cart = Atom.make<CartState>({
68
+ items: [],
69
+ total: 0
70
+ });
71
+ ```
72
+
73
+ ## Pattern: Derived Atoms
74
+
75
+ Use `Atom.map` or computed atoms with the `get` parameter:
76
+
77
+ ```typescript
78
+ // Derived via map
79
+ export const itemCount = Atom.map(cart, (c) => c.items.length);
80
+ export const isEmpty = Atom.map(cart, (c) => c.items.length === 0);
81
+
82
+ // Computed atom accessing other atoms
83
+ export const cartSummary = Atom.make((get) => {
84
+ const cartData = get(cart);
85
+ const count = get(itemCount);
86
+
87
+ return {
88
+ itemCount: count,
89
+ total: cartData.total,
90
+ isEmpty: count === 0
91
+ };
92
+ });
93
+ ```
94
+
95
+ ### Custom Equality
96
+
97
+ Use `Atom.withEquality` when recomputation produces referentially new values that should not notify subscribers when they are semantically unchanged. The default comparison is `Object.is`.
98
+
99
+ ```typescript
100
+ interface Point {
101
+ readonly x: number;
102
+ readonly y: number;
103
+ }
104
+
105
+ export const point = Atom.make<Point>({ x: 0, y: 0 }).pipe(
106
+ Atom.withEquality((current, next) =>
107
+ current.x === next.x && current.y === next.y
108
+ )
109
+ );
110
+ ```
111
+
112
+ Equality changes notification behavior, not computation or mutation. Keep the comparator pure, fast, and consistent; use schema-derived equivalence for schema-modeled domain values.
113
+
114
+ ## Pattern: Atom Family (Dynamic Atoms)
115
+
116
+ Use `Atom.family` for stable references to dynamically created atoms:
117
+
118
+ ```typescript
119
+ // Create atoms per entity ID
120
+ export const userAtoms = Atom.family((userId: string) =>
121
+ Atom.make<User | null>(null).pipe(Atom.keepAlive)
122
+ );
123
+
124
+ // Usage - always returns the same atom for a given ID
125
+ const userAtom = userAtoms(userId);
126
+ ```
127
+
128
+ ## Pattern: Atom.fn for Async Actions
129
+
130
+ Use `Atom.fn` with `Effect.fnUntraced` for async operations:
131
+
132
+ - Reading gives `AsyncResult<Success, Error>` with automatic `.waiting` flag
133
+ - Triggering via `useAtomSet` runs the effect
134
+
135
+ ```typescript
136
+ import * as Atom from "effect/unstable/reactivity/Atom"
137
+ import { useAtomValue, useAtomSet } from "@effect/atom-react"
138
+ import { Effect, Exit } from "effect"
139
+
140
+ // Atom.fn with Effect.fnUntraced for generator syntax
141
+ const logAtom = Atom.fn(
142
+ Effect.fnUntraced(function* (arg: number) {
143
+ yield* Effect.log("got arg", arg)
144
+ })
145
+ )
146
+
147
+ function LogComponent() {
148
+ // useAtomSet returns a trigger function
149
+ const logNumber = useAtomSet(logAtom)
150
+ return <button onClick={() => logNumber(42)}>Log 42</button>
151
+ }
152
+ ```
153
+
154
+ **With services using Atom.runtime:**
155
+
156
+ ```typescript
157
+ class Users extends Context.Service<Users>()("app/Users", {
158
+ make: Effect.gen(function* () {
159
+ const create = (name: string) => Effect.succeed({ id: 1, name })
160
+ return { create } as const
161
+ }),
162
+ }) {}
163
+
164
+ const runtimeAtom = Atom.runtime(Users.layer)
165
+
166
+ // runtimeAtom.fn provides service access
167
+ const createUserAtom = runtimeAtom.fn(
168
+ Effect.fnUntraced(function* (name: string) {
169
+ const users = yield* Users
170
+ return yield* users.create(name)
171
+ })
172
+ )
173
+
174
+ function CreateUserComponent() {
175
+ // mode: "promiseExit" for async handlers with Exit result
176
+ const createUser = useAtomSet(createUserAtom, { mode: "promiseExit" })
177
+ return (
178
+ <button onClick={async () => {
179
+ const exit = await createUser("John")
180
+ if (Exit.isSuccess(exit)) {
181
+ console.log(exit.value)
182
+ }
183
+ }}>
184
+ Create user
185
+ </button>
186
+ )
187
+ }
188
+ ```
189
+
190
+ **Reading result state:**
191
+
192
+ ```typescript
193
+ function UserList() {
194
+ const [result, createUser] = useAtom(createUserAtom) // AsyncResult<User, Error>
195
+
196
+ // Use matchWithWaiting for proper waiting state handling
197
+ return AsyncResult.matchWithWaiting(result, {
198
+ onWaiting: () => <Spinner />,
199
+ onSuccess: ({ value }) => <UserCard user={value} />,
200
+ onError: (error) => <Error message={String(error)} />,
201
+ onDefect: (defect) => <Error message={String(defect)} />
202
+ })
203
+ }
204
+ ```
205
+
206
+ **Anti-pattern: Manual void wrappers**
207
+
208
+ ```typescript
209
+ // ❌ DON'T - manual state management loses waiting control
210
+ const loading$ = Atom.make(false);
211
+ const user$ = Atom.make<User | null>(null);
212
+
213
+ const fetchUser = (id: string): void => {
214
+ registry.set(loading$, true);
215
+ Effect.runPromise(userService.getById(id)).then((user) => {
216
+ registry.set(user$, user);
217
+ registry.set(loading$, false);
218
+ });
219
+ };
220
+
221
+ // ✅ DO - Atom.fn handles loading/success/failure automatically
222
+ const fetchUserAtom = Atom.fn(
223
+ Effect.fnUntraced(function* (id: string) {
224
+ return yield* userService.getById(id);
225
+ })
226
+ );
227
+ // result.waiting, AsyncResult.match - all built-in
228
+ ```
229
+
230
+ ## Pattern: Runtime with Services
231
+
232
+ Wrap Effect layers/services for use in atoms:
233
+
234
+ ```typescript
235
+ import { Layer } from 'effect';
236
+
237
+ // Create runtime with services
238
+ export const runtime = Atom.runtime(
239
+ Layer.mergeAll(DatabaseService.Live, LoggerService.Live, ApiClient.Live)
240
+ );
241
+
242
+ // Use services in function atoms
243
+ export const fetchUserData = runtime.fn(
244
+ Effect.fnUntraced(function* (userId: string) {
245
+ const db = yield* DatabaseService;
246
+ const user = yield* db.getUser(userId);
247
+
248
+ yield* Atom.set(userAtoms(userId), user);
249
+ return user;
250
+ })
251
+ );
252
+ ```
253
+
254
+ ### Global Layers
255
+
256
+ Configure global layers once at app initialization:
257
+
258
+ ```typescript
259
+ // App setup
260
+ Atom.runtime.addGlobalLayer(
261
+ Layer.mergeAll(Logger.Live, Tracer.Live, Config.Live)
262
+ );
263
+ ```
264
+
265
+ ## Pattern: AsyncResult Types (Error Handling)
266
+
267
+ Atoms can return `AsyncResult` types for explicit error handling:
268
+
269
+ ```tsx
270
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
271
+
272
+ export const userData = Atom.make<AsyncResult.AsyncResult<User, Error>>(
273
+ AsyncResult.initial()
274
+ );
275
+
276
+ // In component - use matchWithWaiting for proper waiting state
277
+ const result = useAtomValue(userData);
278
+
279
+ AsyncResult.matchWithWaiting(result, {
280
+ onWaiting: () => <Loading />,
281
+ onSuccess: ({ value }) => <UserProfile user={value} />,
282
+ onError: (error) => <Error message={String(error)} />,
283
+ onDefect: (defect) => <Error message={String(defect)} />
284
+ });
285
+ ```
286
+
287
+ ## Pattern: Stream Integration
288
+
289
+ Convert streams into atoms that capture the latest value:
290
+
291
+ ```typescript
292
+ import { Stream } from 'effect';
293
+
294
+ // Infinite stream becomes reactive atom
295
+ export const notifications = Atom.make(
296
+ Stream.fromEventListener(window, 'notification').pipe(
297
+ Stream.map(parseNotification),
298
+ Stream.filter(isValid),
299
+ Stream.scan([], (acc, n) => [...acc, n].slice(-10))
300
+ )
301
+ );
302
+ ```
303
+
304
+ ## Pattern: Pull Atoms (Pagination)
305
+
306
+ Use `Atom.pull` for stream-based pagination:
307
+
308
+ ```tsx
309
+ export const pagedItems = Atom.pull(
310
+ Stream.fromIterable(itemsSource).pipe(
311
+ Stream.grouped(10) // Pages of 10 items
312
+ )
313
+ );
314
+
315
+ function PagedList() {
316
+ const result = useAtomValue(pagedItems);
317
+ const pullNext = useAtomSet(pagedItems);
318
+
319
+ return AsyncResult.matchWithWaiting(result, {
320
+ onWaiting: () => <Loading />,
321
+ onError: (error) => <Error message={String(error)} />,
322
+ onDefect: (defect) => <Error message={String(defect)} />,
323
+ onSuccess: (success) => (
324
+ <>
325
+ <ItemList items={success.value.items} />
326
+ <button
327
+ disabled={success.value.done}
328
+ onClick={() => pullNext()}
329
+ >
330
+ Load more
331
+ </button>
332
+ </>
333
+ )
334
+ });
335
+ }
336
+ ```
337
+
338
+ `Atom.pull` returns a writable `PullResult`, which is an `AsyncResult` whose success value is `{ done, items }`. The `waiting` flag stays on the top-level result; read pages from `success.value.items` and call `useAtomSet(pagedItems)()` to pull the next chunk.
339
+
340
+ ## Pattern: Persistence
341
+
342
+ Use `Atom.kvs` for persisted state:
343
+
344
+ ```typescript
345
+ import { BrowserKeyValueStore as BrowserKvs } from '@effect/platform-browser';
346
+ import * as Atom from 'effect/unstable/reactivity/Atom';
347
+ import * as Schema from 'effect/Schema';
348
+
349
+ export const userSettings = Atom.kvs({
350
+ runtime: Atom.runtime(BrowserKvs.layerLocalStorage),
351
+ key: 'user-settings',
352
+ schema: Schema.Struct({
353
+ theme: Schema.Literals(['light', 'dark']),
354
+ notifications: Schema.Boolean,
355
+ language: Schema.String
356
+ }),
357
+ defaultValue: () => ({
358
+ theme: 'light',
359
+ notifications: true,
360
+ language: 'en'
361
+ })
362
+ });
363
+ ```
364
+
365
+ If you want to use the core Web Storage layer directly, import the module namespace and pass `Atom.runtime(KeyValueStore.layerStorage(() => globalThis.localStorage))`.
366
+
367
+ ## React Integration
368
+
369
+ ### Hooks
370
+
371
+ ```tsx
372
+ import { useAtomValue, useAtomSet, useAtom } from '@effect/atom-react';
373
+
374
+ export function CartView() {
375
+ // Read only
376
+ const cartData = useAtomValue(cart);
377
+ const isEmpty = useAtomValue(isEmpty);
378
+
379
+ // Write only
380
+ const addItem = useAtomSet(addItem);
381
+ const clearCart = useAtomSet(clearCart);
382
+
383
+ // Both read and write
384
+ const [count, setCount] = useAtom(counterAtom);
385
+
386
+ // For async function atoms (use mode option on useAtomSet)
387
+ const fetchData = useAtomSet(fetchUserData, { mode: 'promiseExit' });
388
+
389
+ return (
390
+ <div>
391
+ <div>Items: {cartData.items.length}</div>
392
+ <button onClick={() => addItem(newItem)}>Add</button>
393
+ <button onClick={() => clearCart()}>Clear</button>
394
+ </div>
395
+ );
396
+ }
397
+ ```
398
+
399
+ ### Separation of Concerns
400
+
401
+ Different components can read/write the same atom reactively:
402
+
403
+ ```tsx
404
+ // Component A - reads state
405
+ function CartDisplay() {
406
+ const cart = useAtomValue(cart);
407
+ return <div>Items: {cart.items.length}</div>;
408
+ }
409
+
410
+ // Component B - modifies state
411
+ function CartActions() {
412
+ const addItem = useAtomSet(addItem);
413
+ return <button onClick={() => addItem(item)}>Add</button>;
414
+ }
415
+
416
+ // Both update reactively when atom changes
417
+ ```
418
+
419
+ ## Scoped Resources & Finalizers
420
+
421
+ Atoms support scoped effects with automatic cleanup:
422
+
423
+ ```typescript
424
+ export const wsConnection = Atom.make(
425
+ Effect.gen(function* () {
426
+ // Acquire resource
427
+ const ws = yield* Effect.acquireRelease(connectWebSocket(), (ws) =>
428
+ Effect.sync(() => ws.close())
429
+ );
430
+
431
+ return ws;
432
+ })
433
+ );
434
+
435
+ // Finalizer runs when atom rebuilds or becomes unused
436
+ ```
437
+
438
+ ## Key Principles
439
+
440
+ 1. **Atom.fn for Async**: Use `Atom.fn()` for effects—gives automatic `waiting` flag and `AsyncResult` type
441
+ 2. **Never Manual Void Wrappers**: Don't wrap Effects in void functions—you lose `waiting` control
442
+ 3. **Reference Stability**: Use `Atom.family` for dynamically generated atom sets
443
+ 4. **Lazy Evaluation**: Values computed on-demand when accessed
444
+ 5. **Automatic Cleanup**: Atoms reset when unused (unless `keepAlive`)
445
+ 6. **Derive, Don't Coordinate**: Use computed atoms to derive state
446
+ 7. **Result Types**: Handle errors explicitly with AsyncResult.match
447
+ 8. **Services in Runtime**: Wrap layers once, use in multiple atoms
448
+ 9. **Immutable Updates**: Always create new values, never mutate
449
+ 10. **Scoped Effects**: Leverage finalizers for resource cleanup
450
+ 11. **Intentional Equality**: Use `Atom.withEquality` to suppress semantically redundant notifications
451
+
452
+ ## Common Patterns
453
+
454
+ ### Loading States
455
+
456
+ Use `Atom.fn` with `Effect.fnUntraced` which automatically provides `AsyncResult` with `.waiting` flag:
457
+
458
+ ```typescript
459
+ import * as Atom from "effect/unstable/reactivity/Atom"
460
+ import { useAtomValue, useAtomSet } from "@effect/atom-react"
461
+ import { Effect } from "effect"
462
+
463
+ // Atom.fn handles loading/success/failure automatically
464
+ const loadUserAtom = Atom.fn(
465
+ Effect.fnUntraced(function* (id: string) {
466
+ return yield* userService.fetchUser(id)
467
+ })
468
+ )
469
+
470
+ // In component
471
+ function UserProfile() {
472
+ const [result, loadUser] = useAtom(loadUserAtom)
473
+
474
+ // Use matchWithWaiting for proper waiting state handling
475
+ return AsyncResult.matchWithWaiting(result, {
476
+ onWaiting: () => <Loading />,
477
+ onSuccess: ({ value }) => <UserCard user={value} />,
478
+ onError: (error) => <Error message={String(error)} />,
479
+ onDefect: (defect) => <Error message={String(defect)} />
480
+ })
481
+ }
482
+ ```
483
+
484
+ ### Optimistic Updates
485
+
486
+ ```typescript
487
+ export const updateItem = runtime.fn(
488
+ Effect.fnUntraced(function* (id: string, updates: Partial<Item>) {
489
+ const current = yield* Atom.get(itemsAtom);
490
+
491
+ // Optimistic update
492
+ yield* Atom.set(
493
+ itemsAtom,
494
+ current.map((item) =>
495
+ item.id === id ? { ...item, ...updates } : item
496
+ )
497
+ );
498
+
499
+ // Persist to server
500
+ const result = yield* Effect.result(api.updateItem(id, updates));
501
+
502
+ // Revert on failure
503
+ if (result._tag === 'Failure') {
504
+ yield* Atom.set(itemsAtom, current);
505
+ }
506
+ })
507
+ );
508
+ ```
509
+
510
+ ### Computed Queries
511
+
512
+ ```typescript
513
+ // Filter atom accessing other atoms
514
+ export const filteredItems = Atom.make((get) => {
515
+ const items = get(itemsAtom);
516
+ const searchTerm = get(searchAtom);
517
+ const activeFilters = get(filtersAtom);
518
+
519
+ return items.filter(
520
+ (item) =>
521
+ item.name.includes(searchTerm) &&
522
+ activeFilters.every((f) => f.predicate(item))
523
+ );
524
+ });
525
+ ```
526
+
527
+ ## External push sources
528
+
529
+ For browser APIs or other external sources that push updates, create an atom with `Atom.make((get) => ...)` and use `get.setSelf` plus `get.addFinalizer`:
530
+
531
+ ```typescript
532
+ // System theme detection — updates reactively via matchMedia listener
533
+ export const systemThemeAtom = Atom.make((get) => {
534
+ const mql = window.matchMedia('(prefers-color-scheme: dark)');
535
+ const readTheme = (): 'light' | 'dark' =>
536
+ mql.matches ? 'dark' : 'light';
537
+
538
+ const handler = (event: MediaQueryListEvent) => {
539
+ get.setSelf(event.matches ? 'dark' : 'light');
540
+ };
541
+
542
+ mql.addEventListener('change', handler);
543
+ get.addFinalizer(() => mql.removeEventListener('change', handler));
544
+
545
+ return readTheme();
546
+ });
547
+ ```
548
+
549
+ Use `Atom.transform` only to transform an existing source atom; it is not a standalone constructor that accepts an initial value and subscription effect.
550
+
551
+ ## Atom.batch
552
+
553
+ Batch multiple atom updates into a single notification cycle:
554
+
555
+ ```typescript
556
+ Atom.batch(() => {
557
+ registry.set(nameAtom, 'Alice');
558
+ registry.set(ageAtom, 30);
559
+ registry.set(statusAtom, 'active');
560
+ });
561
+ // Subscribers notified once, not three times
562
+ ```
563
+
564
+ Outside Effect/Atom contexts, use an `AtomRegistry` (`registry.set(...)`) inside the batch. Inside an atom or write context, use that context (`ctx.set(...)`) in the same pattern. `Atom.batch` only batches notifications; it does not introduce a free `set` function.
565
+
566
+ Use when multiple atoms must update atomically to avoid intermediate renders.
567
+
568
+ ## AsyncResult.builder
569
+
570
+ Chainable API for handling `AsyncResult` types — replaces verbose `AsyncResult.match`/`AsyncResult.matchWithWaiting`:
571
+
572
+ ```typescript
573
+ const UserProfile = ({ userId }: { userId: string }) => {
574
+ const user = useAtomValue(userAtom(userId))
575
+
576
+ return AsyncResult.builder(user)
577
+ .onInitial(() => <LoadingSkeleton />)
578
+ .onErrorTag('UserNotFound', (err) => <NotFound id={err.userId} />)
579
+ .onErrorIf(
580
+ (err): err is NetworkError => err instanceof NetworkError,
581
+ () => <RetryPrompt />
582
+ )
583
+ .onError((err) => <Error message={String(err)} />)
584
+ .onSuccess((user) => <ProfileCard user={user} />)
585
+ .render()
586
+ }
587
+ ```
588
+
589
+ - `onInitial` — initial/pending state
590
+ - `onError` — handle any typed error value
591
+ - `onErrorIf` — handle typed errors with a predicate or refinement
592
+ - `onErrorTag` — handle tagged errors by `_tag`
593
+ - `onFailure` — receives the whole `Cause.Cause<E>`; reserve it for cause-level fallback handling
594
+ - `onSuccess` — render the success value
595
+ - `render()` — finalize and return JSX; it throws unhandled failures, so handle every expected error/defect or use `orElse` / `orNull`
596
+
597
+ ## useAtomMount
598
+
599
+ Activate a side-effect atom without reading its value:
600
+
601
+ ```typescript
602
+ // Start a WebSocket connection when component mounts, clean up on unmount
603
+ useAtomMount(websocketAtom);
604
+
605
+ // Start polling without consuming the value
606
+ useAtomMount(pollingAtom);
607
+ ```
608
+
609
+ Use when an atom's side effects matter but its value doesn't need to be rendered.
610
+
611
+ ## Performance
612
+
613
+ ### Selective Re-rendering
614
+
615
+ Derive focused atoms to avoid unnecessary re-renders:
616
+
617
+ ```typescript
618
+ // Bad: entire component re-renders when any user field changes
619
+ const user = useAtomValue(userAtom)
620
+ return <span>{user.name}</span>
621
+
622
+ // Good: only re-renders when name changes
623
+ const userName = useMemo(() => Atom.map(userAtom, (u) => u.name), [])
624
+ const name = useAtomValue(userName)
625
+ return <span>{name}</span>
626
+ ```
627
+
628
+ Use `Atom.map` to create narrow slices of state that minimize re-render surface.
629
+
630
+ ## Anti-Patterns
631
+
632
+ ```
633
+ atoms inside components → creates new atom every render; define outside or useMemo
634
+ missing finalizers → memory/subscription leaks; always clean up in transform/fn
635
+ missing keepAlive → global state garbage collected; use Atom.keepAlive
636
+ ignoring AsyncResult types → crashes on error states; always handle all AsyncResult variants
637
+ updating state during render → infinite loops; use effects or event handlers
638
+ ```
639
+
640
+ Effect Atom bridges Effect's powerful type system with React's rendering model, providing type-safe reactive state management with automatic cleanup and seamless Effect integration.