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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- 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.
|