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,986 @@
1
+ ---
2
+ name: effect-react-composition
3
+ description: Build composable React components using Effect Atom for state management. Use this skill when implementing React UIs that avoid boolean props, embrace component composition, and integrate with Effect's reactive state system.
4
+ ---
5
+
6
+ # React Composition Skill
7
+
8
+ Build React UIs using compositional patterns, Effect Atom for state management, and the component module pattern. Use this skill when creating React applications that integrate with Effect's ecosystem.
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
+ ## When to Use This Skill
22
+
23
+ - Building React components that integrate with Effect Atom state
24
+ - Refactoring components away from boolean prop anti-patterns
25
+ - Implementing complex UIs through composition of simple pieces
26
+ - Creating reusable component libraries with flexible APIs
27
+ - Managing shared state across multiple React components
28
+ - Lifting state to appropriate levels in component trees
29
+
30
+ ## Core Principles
31
+
32
+ ### 1. Composition over Configuration
33
+
34
+ Build complex UIs from simple, composable pieces rather than configuring behavior through props.
35
+
36
+ **Anti-Pattern: Boolean Props**
37
+
38
+ ```tsx
39
+ // L WRONG - Configuration through boolean props
40
+ interface FormProps {
41
+ isUpdate?: boolean;
42
+ hideWelcome?: boolean;
43
+ showEmail?: boolean;
44
+ redirectOnSuccess?: boolean;
45
+ enableValidation?: boolean;
46
+ }
47
+
48
+ function UserForm({
49
+ isUpdate,
50
+ hideWelcome,
51
+ showEmail,
52
+ redirectOnSuccess,
53
+ enableValidation
54
+ }: FormProps) {
55
+ return (
56
+ <form>
57
+ {!hideWelcome && <WelcomeMessage />}
58
+ <NameField />
59
+ {showEmail && <EmailField />}
60
+ <button type="submit">{isUpdate ? 'Update' : 'Create'}</button>
61
+ </form>
62
+ );
63
+ }
64
+
65
+ // Usage becomes unreadable
66
+ <UserForm isUpdate hideWelcome showEmail redirectOnSuccess />;
67
+ ```
68
+
69
+ **Correct Pattern: Composition**
70
+
71
+ ```tsx
72
+ //  CORRECT - Compose specific forms from atomic pieces
73
+ export namespace UserForm {
74
+ export const Frame: React.FC<{ children: React.ReactNode }> = ({
75
+ children
76
+ }) => <form className="user-form">{children}</form>;
77
+
78
+ export const WelcomeMessage: React.FC = () => (
79
+ <div className="welcome">Welcome!</div>
80
+ );
81
+
82
+ export const NameField: React.FC = () => (
83
+ <input name="name" placeholder="Name" />
84
+ );
85
+
86
+ export const EmailField: React.FC = () => (
87
+ <input type="email" name="email" placeholder="Email" />
88
+ );
89
+
90
+ export const SubmitButton: React.FC<{ children: React.ReactNode }> = ({
91
+ children
92
+ }) => <button type="submit">{children}</button>;
93
+ }
94
+
95
+ // Create specific forms through composition
96
+ function CreateUserForm() {
97
+ return (
98
+ <UserForm.Frame>
99
+ <UserForm.WelcomeMessage />
100
+ <UserForm.NameField />
101
+ <UserForm.EmailField />
102
+ <UserForm.SubmitButton>Create</UserForm.SubmitButton>
103
+ </UserForm.Frame>
104
+ );
105
+ }
106
+
107
+ function UpdateUserForm() {
108
+ return (
109
+ <UserForm.Frame>
110
+ <UserForm.NameField />
111
+ <UserForm.SubmitButton>Update</UserForm.SubmitButton>
112
+ </UserForm.Frame>
113
+ );
114
+ }
115
+
116
+ // Usage is clear and explicit (in JSX context):
117
+ // <CreateUserForm />
118
+ // <UpdateUserForm />
119
+ ```
120
+
121
+ ### 2. Component Module Pattern
122
+
123
+ Treat components like Effect modules with namespace imports and exported sub-components.
124
+
125
+ ```tsx
126
+ // components/Composer/Composer.tsx
127
+ import * as React from 'react';
128
+
129
+ /**
130
+ * Composer state interface
131
+ */
132
+ export interface ComposerState {
133
+ readonly content: string;
134
+ readonly attachments: ReadonlyArray<Attachment>;
135
+ readonly isSubmitting: boolean;
136
+ }
137
+
138
+ /**
139
+ * Composer context for sharing state
140
+ */
141
+ const ComposerContext = React.createContext<ComposerState | null>(null);
142
+
143
+ /**
144
+ * Hook to access composer state
145
+ * @throws when used outside Provider
146
+ */
147
+ export const useComposer = (): ComposerState => {
148
+ const context = React.useContext(ComposerContext);
149
+ if (!context) {
150
+ throw new Error('useComposer must be used within Composer.Provider');
151
+ }
152
+ return context;
153
+ };
154
+
155
+ /**
156
+ * Provider component for composer state
157
+ */
158
+ export const Provider: React.FC<{
159
+ children: React.ReactNode;
160
+ state: ComposerState;
161
+ }> = ({ children, state }) => (
162
+ <ComposerContext.Provider value={state}>
163
+ {children}
164
+ </ComposerContext.Provider>
165
+ );
166
+
167
+ /**
168
+ * Frame component for layout
169
+ */
170
+ export const Frame: React.FC<{
171
+ children: React.ReactNode;
172
+ }> = ({ children }) => <div className="composer-frame">{children}</div>;
173
+
174
+ /**
175
+ * Input component for message content
176
+ */
177
+ export const Input: React.FC = () => {
178
+ const { content } = useComposer();
179
+ return (
180
+ <textarea
181
+ value={content}
182
+ className="composer-input"
183
+ placeholder="Type a message..."
184
+ />
185
+ );
186
+ };
187
+
188
+ /**
189
+ * Footer component for actions
190
+ */
191
+ export const Footer: React.FC<{
192
+ children: React.ReactNode;
193
+ }> = ({ children }) => <div className="composer-footer">{children}</div>;
194
+
195
+ /**
196
+ * Submit button component
197
+ */
198
+ export const Submit: React.FC = () => {
199
+ const { isSubmitting } = useComposer();
200
+ return (
201
+ <button
202
+ type="submit"
203
+ disabled={isSubmitting}
204
+ className="composer-submit"
205
+ >
206
+ {isSubmitting ? 'Sending...' : 'Send'}
207
+ </button>
208
+ );
209
+ };
210
+ ```
211
+
212
+ **Usage with Namespace Import**
213
+
214
+ ```tsx
215
+ import * as Composer from '@/components/Composer';
216
+
217
+ function MessageComposer() {
218
+ const [state, setState] = useState<Composer.ComposerState>({
219
+ content: '',
220
+ attachments: [],
221
+ isSubmitting: false
222
+ });
223
+
224
+ return (
225
+ <Composer.Provider state={state}>
226
+ <Composer.Frame>
227
+ <Composer.Input />
228
+ <Composer.Footer>
229
+ <Composer.Submit />
230
+ </Composer.Footer>
231
+ </Composer.Frame>
232
+ </Composer.Provider>
233
+ );
234
+ }
235
+ ```
236
+
237
+ ### 3. State Lifting Pattern
238
+
239
+ Lift state ABOVE all components that need access, not below.
240
+
241
+ **Anti-Pattern: State Too Low**
242
+
243
+ ```tsx
244
+ // L WRONG - State below components that need it
245
+ function Modal() {
246
+ return (
247
+ <ModalFrame>
248
+ <ModalContent>
249
+ <Composer /> {/* State is here */}
250
+ </ModalContent>
251
+ <ModalFooter>
252
+ <ExternalButton /> {/* Cannot access Composer state! */}
253
+ </ModalFooter>
254
+ </ModalFrame>
255
+ );
256
+ }
257
+ ```
258
+
259
+ **Correct Pattern: Lift State Above**
260
+
261
+ ```tsx
262
+ //  CORRECT - State above everything that needs it
263
+ function Modal() {
264
+ const [state, setState] = useState(initialComposerState);
265
+
266
+ // Provider wraps EVERYTHING that needs access
267
+ return (
268
+ <Composer.Provider state={state}>
269
+ <ModalFrame>
270
+ <ModalContent>
271
+ <Composer.Input />
272
+ </ModalContent>
273
+ <ModalFooter>
274
+ <ExternalButton /> {/* Can access state via useComposer! */}
275
+ <Composer.Submit />
276
+ </ModalFooter>
277
+ </ModalFrame>
278
+ </Composer.Provider>
279
+ );
280
+ }
281
+
282
+ function ExternalButton() {
283
+ const { content } = Composer.useComposer();
284
+ const hasContent = content.length > 0;
285
+
286
+ return <button disabled={!hasContent}>Preview</button>;
287
+ }
288
+ ```
289
+
290
+ ## Effect Atom Integration
291
+
292
+ Effect Atom provides reactive state management that integrates seamlessly with React.
293
+
294
+ ### Pattern: Basic Atom State
295
+
296
+ ```typescript
297
+ // state/Cart.ts
298
+ import * as Atom from 'effect/unstable/reactivity/Atom';
299
+ import { Effect } from 'effect';
300
+
301
+ /**
302
+ * Cart item interface
303
+ */
304
+ export interface CartItem {
305
+ readonly id: string;
306
+ readonly name: string;
307
+ readonly price: number;
308
+ readonly quantity: number;
309
+ }
310
+
311
+ /**
312
+ * Cart state interface
313
+ */
314
+ export interface CartState {
315
+ readonly items: ReadonlyArray<CartItem>;
316
+ readonly total: number;
317
+ }
318
+
319
+ /**
320
+ * Main cart atom
321
+ */
322
+ export const cart = Atom.make<CartState>({
323
+ items: [],
324
+ total: 0
325
+ });
326
+
327
+ /**
328
+ * Derived atom: item count
329
+ */
330
+ export const itemCount = Atom.map(cart, (c) => c.items.length);
331
+
332
+ /**
333
+ * Derived atom: is cart empty
334
+ */
335
+ export const isEmpty = Atom.map(cart, (c) => c.items.length === 0);
336
+
337
+ /**
338
+ * Stable summary: suppress notifications when a rebuilt summary is equal.
339
+ */
340
+ export const summary = Atom.make((get) => {
341
+ const value = get(cart);
342
+ return { itemCount: value.items.length, total: value.total };
343
+ }).pipe(
344
+ Atom.withEquality(
345
+ (current, next) =>
346
+ current.itemCount === next.itemCount && current.total === next.total
347
+ )
348
+ );
349
+
350
+ /**
351
+ * Add item to cart
352
+ */
353
+ export const addItem = Atom.fn(
354
+ Effect.fnUntraced(function* (item: CartItem) {
355
+ const current = yield* Atom.get(cart);
356
+
357
+ const existingIndex = current.items.findIndex((i) => i.id === item.id);
358
+
359
+ if (existingIndex >= 0) {
360
+ // Update quantity
361
+ const updatedItems = [...current.items];
362
+ updatedItems[existingIndex] = {
363
+ ...updatedItems[existingIndex],
364
+ quantity: updatedItems[existingIndex].quantity + item.quantity
365
+ };
366
+
367
+ yield* Atom.set(cart, {
368
+ items: updatedItems,
369
+ total: current.total + item.price * item.quantity
370
+ });
371
+ } else {
372
+ // Add new item
373
+ yield* Atom.set(cart, {
374
+ items: [...current.items, item],
375
+ total: current.total + item.price * item.quantity
376
+ });
377
+ }
378
+ })
379
+ );
380
+
381
+ /**
382
+ * Remove item from cart
383
+ */
384
+ export const removeItem = Atom.fn(
385
+ Effect.fnUntraced(function* (itemId: string) {
386
+ const current = yield* Atom.get(cart);
387
+ const item = current.items.find((i) => i.id === itemId);
388
+
389
+ if (!item) return;
390
+
391
+ yield* Atom.set(cart, {
392
+ items: current.items.filter((i) => i.id !== itemId),
393
+ total: current.total - item.price * item.quantity
394
+ });
395
+ })
396
+ );
397
+
398
+ /**
399
+ * Clear cart
400
+ */
401
+ export const clearCart = Atom.fn(
402
+ Effect.fnUntraced(function* () {
403
+ yield* Atom.set(cart, { items: [], total: 0 });
404
+ })
405
+ );
406
+ ```
407
+
408
+ ### Pattern: React Component with Atoms
409
+
410
+ ```tsx
411
+ // components/Cart/CartView.tsx
412
+ import { useAtomValue, useAtomSet } from '@effect/atom-react';
413
+ import * as Cart from '@/state/Cart';
414
+
415
+ /**
416
+ * Cart display component
417
+ */
418
+ export function CartView() {
419
+ const cartData = useAtomValue(Cart.cart);
420
+ const count = useAtomValue(Cart.itemCount);
421
+ const empty = useAtomValue(Cart.isEmpty);
422
+ const removeItem = useAtomSet(Cart.removeItem);
423
+ const clearCart = useAtomSet(Cart.clearCart);
424
+
425
+ if (empty) {
426
+ return <div className="cart-empty">Your cart is empty</div>;
427
+ }
428
+
429
+ return (
430
+ <div className="cart">
431
+ <h2>Cart ({count} items)</h2>
432
+
433
+ <ul className="cart-items">
434
+ {cartData.items.map((item) => (
435
+ <li key={item.id} className="cart-item">
436
+ <span>{item.name}</span>
437
+ <span>
438
+ {item.quantity} x ${item.price}
439
+ </span>
440
+ <button onClick={() => removeItem(item.id)}>
441
+ Remove
442
+ </button>
443
+ </li>
444
+ ))}
445
+ </ul>
446
+
447
+ <div className="cart-total">Total: ${cartData.total}</div>
448
+
449
+ <button onClick={() => clearCart()}>Clear Cart</button>
450
+ </div>
451
+ );
452
+ }
453
+ ```
454
+
455
+ ### Pattern: Separation of Concerns
456
+
457
+ Different components can read/write the same atom reactively:
458
+
459
+ ```tsx
460
+ // Component A - Read only
461
+ function CartBadge() {
462
+ const count = useAtomValue(Cart.itemCount);
463
+
464
+ return (
465
+ <div className="cart-badge">{count > 0 && <span>{count}</span>}</div>
466
+ );
467
+ }
468
+
469
+ // Component B - Write only
470
+ function AddToCartButton({ item }: { item: CartItem }) {
471
+ const addItem = useAtomSet(Cart.addItem);
472
+
473
+ return <button onClick={() => addItem(item)}>Add to Cart</button>;
474
+ }
475
+
476
+ // Component C - Read and write
477
+ function CartControls() {
478
+ const [cartState, setCart] = useAtom(Cart.cart);
479
+
480
+ return (
481
+ <div>
482
+ <span>Items: {cartState.items.length}</span>
483
+ <button onClick={() => setCart({ items: [], total: 0 })}>
484
+ Reset
485
+ </button>
486
+ </div>
487
+ );
488
+ }
489
+
490
+ // All components update reactively when atom changes
491
+ ```
492
+
493
+ ### Pattern: Async Operations with Atoms
494
+
495
+ Use `runtime.atom` and `Atom.family` for query-like data. The runtime wraps the Effect result as `AsyncResult` automatically, so you do not need a separate result atom or a `useEffect` trigger.
496
+
497
+ ```typescript
498
+ // state/User.ts
499
+ import * as Atom from 'effect/unstable/reactivity/Atom';
500
+ import { Effect } from 'effect';
501
+ import { UserService } from '@/services/UserService';
502
+
503
+ /**
504
+ * Runtime with UserService
505
+ */
506
+ const runtime = Atom.runtime(UserService.Live);
507
+
508
+ /**
509
+ * User data keyed by id. Each atom value is AsyncResult<User, UserError>.
510
+ */
511
+ export const userData = Atom.family((userId: string) =>
512
+ runtime.atom(
513
+ Effect.gen(function* () {
514
+ const userService = yield* UserService;
515
+ return yield* userService.getUser(userId);
516
+ })
517
+ )
518
+ );
519
+ ```
520
+
521
+ **Component with AsyncResult Handling**
522
+
523
+ ```tsx
524
+ import { useAtomValue } from '@effect/atom-react';
525
+ import * as AsyncResult from 'effect/unstable/reactivity/AsyncResult';
526
+ import * as User from '@/state/User';
527
+
528
+ function UserProfile({ userId }: { userId: string }) {
529
+ const result = useAtomValue(User.userData(userId));
530
+
531
+ return AsyncResult.matchWithWaiting(result, {
532
+ onWaiting: () => <Loading />,
533
+ onError: (error) => <Error message={String(error)} />,
534
+ onDefect: (defect) => <Error message={String(defect)} />,
535
+ onSuccess: ({ value: user }) => (
536
+ <div className="user-profile">
537
+ <h2>{user.name}</h2>
538
+ <p>{user.email}</p>
539
+ </div>
540
+ )
541
+ });
542
+ }
543
+ ```
544
+
545
+ ## Avoiding useEffect
546
+
547
+ Most `useEffect` usage is wrong. Consider these alternatives:
548
+
549
+ ### Anti-Pattern: Unnecessary Effects
550
+
551
+ ```tsx
552
+ // L WRONG - Using effect for derived state
553
+ function UserCard({ user }: { user: User }) {
554
+ const [fullName, setFullName] = useState('');
555
+
556
+ useEffect(() => {
557
+ setFullName(`${user.firstName} ${user.lastName}`);
558
+ }, [user]);
559
+
560
+ return <div>{fullName}</div>;
561
+ }
562
+
563
+ //  CORRECT - Calculate during render
564
+ function UserCard({ user }: { user: User }) {
565
+ const fullName = `${user.firstName} ${user.lastName}`;
566
+ return <div>{fullName}</div>;
567
+ }
568
+ ```
569
+
570
+ ### Anti-Pattern: Effect for Expensive Computation
571
+
572
+ ```tsx
573
+ // L WRONG - Effect for memoization
574
+ function ProductList({ products }: { products: Product[] }) {
575
+ const [filtered, setFiltered] = useState<Product[]>([]);
576
+
577
+ useEffect(() => {
578
+ setFiltered(products.filter(expensiveFilter));
579
+ }, [products]);
580
+
581
+ return <div>{filtered.map(renderProduct)}</div>;
582
+ }
583
+
584
+ //  CORRECT - useMemo for expensive computation
585
+ function ProductList({ products }: { products: Product[] }) {
586
+ const filtered = useMemo(
587
+ () => products.filter(expensiveFilter),
588
+ [products]
589
+ );
590
+
591
+ return <div>{filtered.map(renderProduct)}</div>;
592
+ }
593
+ ```
594
+
595
+ ### Pattern: useTransition for Non-Blocking Updates
596
+
597
+ ```tsx
598
+ import { useTransition } from 'react';
599
+
600
+ function SearchResults() {
601
+ const [query, setQuery] = useState('');
602
+ const [isPending, startTransition] = useTransition();
603
+
604
+ const handleSearch = (value: string) => {
605
+ setQuery(value); // Immediate update
606
+
607
+ // Non-blocking update for expensive operation
608
+ startTransition(() => {
609
+ // Expensive filter/sort operation
610
+ updateSearchResults(value);
611
+ });
612
+ };
613
+
614
+ return (
615
+ <div>
616
+ <input onChange={(e) => handleSearch(e.target.value)} />
617
+ {isPending && <LoadingSpinner />}
618
+ <Results />
619
+ </div>
620
+ );
621
+ }
622
+ ```
623
+
624
+ ### Pattern: Keys for Resetting State
625
+
626
+ ```tsx
627
+ // L WRONG - Effect to reset state
628
+ function UserEditor({ userId }: { userId: string }) {
629
+ const [formData, setFormData] = useState(initialData);
630
+
631
+ useEffect(() => {
632
+ setFormData(initialData); // Reset on user change
633
+ }, [userId]);
634
+
635
+ return <form>...</form>;
636
+ }
637
+
638
+ //  CORRECT - Use key to reset component
639
+ function UserEditor({ userId }: { userId: string }) {
640
+ return <UserEditorForm key={userId} />;
641
+ }
642
+
643
+ function UserEditorForm() {
644
+ const [formData, setFormData] = useState(initialData);
645
+ // State automatically resets when key changes
646
+
647
+ return <form>...</form>;
648
+ }
649
+ ```
650
+
651
+ ### When useEffect is Appropriate
652
+
653
+ Use `useEffect` for:
654
+
655
+ - Synchronizing with external systems (WebSocket, DOM APIs)
656
+ - Side effects that must run after render
657
+ - Cleanup of subscriptions
658
+
659
+ ```tsx
660
+ function ChatRoom({ roomId }: { roomId: string }) {
661
+ useEffect(() => {
662
+ // Connect to external system
663
+ const connection = connectToRoom(roomId);
664
+
665
+ // Cleanup on unmount or roomId change
666
+ return () => {
667
+ connection.disconnect();
668
+ };
669
+ }, [roomId]);
670
+
671
+ return <div>...</div>;
672
+ }
673
+ ```
674
+
675
+ ## Advanced Patterns
676
+
677
+ ### Pattern: Render Props for Flexibility
678
+
679
+ ```tsx
680
+ export namespace DataTable {
681
+ export interface RenderProps<T> {
682
+ readonly data: ReadonlyArray<T>;
683
+ readonly isLoading: boolean;
684
+ }
685
+
686
+ export const Frame: React.FC<{
687
+ children: React.ReactNode;
688
+ }> = ({ children }) => <div className="data-table">{children}</div>;
689
+
690
+ export const Header: React.FC<{
691
+ columns: ReadonlyArray<string>;
692
+ }> = ({ columns }) => (
693
+ <thead>
694
+ <tr>
695
+ {columns.map((col) => (
696
+ <th key={col}>{col}</th>
697
+ ))}
698
+ </tr>
699
+ </thead>
700
+ );
701
+
702
+ export const Body: React.FC<{
703
+ children: (props: RenderProps<T>) => React.ReactNode;
704
+ }> = ({ children, data, isLoading }) => (
705
+ <tbody>{children({ data, isLoading })}</tbody>
706
+ );
707
+ }
708
+
709
+ // Usage
710
+ function UserTable() {
711
+ const users = useAtomValue(User.list);
712
+
713
+ return (
714
+ <DataTable.Frame>
715
+ <DataTable.Header columns={['Name', 'Email']} />
716
+ <DataTable.Body>
717
+ {({ data, isLoading }) =>
718
+ isLoading ? (
719
+ <LoadingRow />
720
+ ) : (
721
+ data.map((user) => (
722
+ <UserRow key={user.id} user={user} />
723
+ ))
724
+ )
725
+ }
726
+ </DataTable.Body>
727
+ </DataTable.Frame>
728
+ );
729
+ }
730
+ ```
731
+
732
+ ### Pattern: Compound Components with Context
733
+
734
+ ```tsx
735
+ export namespace Tabs {
736
+ interface TabsContext {
737
+ readonly activeTab: string;
738
+ readonly setActiveTab: (tab: string) => void;
739
+ }
740
+
741
+ const Context = React.createContext<TabsContext | null>(null);
742
+
743
+ export const Provider: React.FC<{
744
+ children: React.ReactNode;
745
+ defaultTab: string;
746
+ }> = ({ children, defaultTab }) => {
747
+ const [activeTab, setActiveTab] = useState(defaultTab);
748
+
749
+ return (
750
+ <Context.Provider value={{ activeTab, setActiveTab }}>
751
+ {children}
752
+ </Context.Provider>
753
+ );
754
+ };
755
+
756
+ export const List: React.FC<{
757
+ children: React.ReactNode;
758
+ }> = ({ children }) => <div role="tablist">{children}</div>;
759
+
760
+ export const Tab: React.FC<{
761
+ id: string;
762
+ children: React.ReactNode;
763
+ }> = ({ id, children }) => {
764
+ const context = React.useContext(Context);
765
+ if (!context) throw new Error('Tab must be within Provider');
766
+
767
+ const isActive = context.activeTab === id;
768
+
769
+ return (
770
+ <button
771
+ role="tab"
772
+ aria-selected={isActive}
773
+ onClick={() => context.setActiveTab(id)}
774
+ >
775
+ {children}
776
+ </button>
777
+ );
778
+ };
779
+
780
+ export const Panel: React.FC<{
781
+ id: string;
782
+ children: React.ReactNode;
783
+ }> = ({ id, children }) => {
784
+ const context = React.useContext(Context);
785
+ if (!context) throw new Error('Panel must be within Provider');
786
+
787
+ if (context.activeTab !== id) return null;
788
+
789
+ return <div role="tabpanel">{children}</div>;
790
+ };
791
+ }
792
+
793
+ // Usage
794
+ function Settings() {
795
+ return (
796
+ <Tabs.Provider defaultTab="general">
797
+ <Tabs.List>
798
+ <Tabs.Tab id="general">General</Tabs.Tab>
799
+ <Tabs.Tab id="security">Security</Tabs.Tab>
800
+ <Tabs.Tab id="notifications">Notifications</Tabs.Tab>
801
+ </Tabs.List>
802
+
803
+ <Tabs.Panel id="general">
804
+ <GeneralSettings />
805
+ </Tabs.Panel>
806
+ <Tabs.Panel id="security">
807
+ <SecuritySettings />
808
+ </Tabs.Panel>
809
+ <Tabs.Panel id="notifications">
810
+ <NotificationSettings />
811
+ </Tabs.Panel>
812
+ </Tabs.Provider>
813
+ );
814
+ }
815
+ ```
816
+
817
+ ## Testing Compositional Components
818
+
819
+ ### Pattern: Test Atomic Components
820
+
821
+ ```tsx
822
+ import { render, screen } from '@testing-library/react';
823
+ import * as Composer from '@/components/Composer';
824
+
825
+ describe('Composer.Input', () => {
826
+ it('displays content from context', () => {
827
+ const state: Composer.ComposerState = {
828
+ content: 'Hello world',
829
+ attachments: [],
830
+ isSubmitting: false
831
+ };
832
+
833
+ render(
834
+ <Composer.Provider state={state}>
835
+ <Composer.Input />
836
+ </Composer.Provider>
837
+ );
838
+
839
+ expect(screen.getByDisplayValue('Hello world')).toBeInTheDocument();
840
+ });
841
+ });
842
+ ```
843
+
844
+ ### Pattern: Test Composed Features
845
+
846
+ ```tsx
847
+ describe('MessageComposer', () => {
848
+ it('composes correctly', () => {
849
+ render(<MessageComposer />);
850
+
851
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
852
+ expect(
853
+ screen.getByRole('button', { name: 'Send' })
854
+ ).toBeInTheDocument();
855
+ });
856
+
857
+ it('disables submit when submitting', () => {
858
+ const state: Composer.ComposerState = {
859
+ content: 'Hello',
860
+ attachments: [],
861
+ isSubmitting: true
862
+ };
863
+
864
+ render(
865
+ <Composer.Provider state={state}>
866
+ <Composer.Submit />
867
+ </Composer.Provider>
868
+ );
869
+
870
+ expect(screen.getByRole('button')).toBeDisabled();
871
+ });
872
+ });
873
+ ```
874
+
875
+ ## Quality Checklist
876
+
877
+ When implementing React components with Effect Atom, ensure:
878
+
879
+ - [ ] No boolean props - use composition instead
880
+ - [ ] Components organized in namespaces with namespace imports
881
+ - [ ] State lifted to appropriate level (above all components that need it)
882
+ - [ ] Atomic components compose into features
883
+ - [ ] Effect Atom used for shared/complex state
884
+ - [ ] `Atom.withEquality` used when derived object identity would cause redundant renders
885
+ - [ ] Avoid unnecessary `useEffect` - prefer direct calculation, `useMemo`, or `useTransition`
886
+ - [ ] AsyncResult types used for async operations with explicit error handling
887
+ - [ ] Context only for component-specific state, Atoms for app-wide state
888
+ - [ ] All exports documented with JSDoc
889
+ - [ ] Components testable in isolation
890
+ - [ ] Render props or compound components for maximum flexibility
891
+
892
+ ## Common Mistakes
893
+
894
+ ### Mistake: Mixing Context and Atoms
895
+
896
+ ```tsx
897
+ // L WRONG - Using Context for app-wide state
898
+ const AppContext = React.createContext<AppState | null>(null);
899
+
900
+ function App() {
901
+ const [state, setState] = useState(appState);
902
+ return (
903
+ <AppContext.Provider value={state}>
904
+ {/* Deep component tree */}
905
+ </AppContext.Provider>
906
+ );
907
+ }
908
+
909
+ //  CORRECT - Use Atoms for app-wide state
910
+ // state/App.ts
911
+ export const appState = Atom.make<AppState>(initialState);
912
+
913
+ // Components access directly
914
+ function DeepComponent() {
915
+ const state = useAtomValue(appState);
916
+ return <div>{state.value}</div>;
917
+ }
918
+ ```
919
+
920
+ ### Mistake: Not Lifting State High Enough
921
+
922
+ ```tsx
923
+ // L WRONG - State trapped in Modal
924
+ function Modal() {
925
+ return (
926
+ <div className="modal">
927
+ <Editor /> {/* State is here */}
928
+ <Footer>
929
+ <SaveButton /> {/* Cannot access Editor state */}
930
+ </Footer>
931
+ </div>
932
+ );
933
+ }
934
+
935
+ //  CORRECT - Lift state above Modal
936
+ function ModalContainer() {
937
+ const editorState = useAtomValue(Editor.state);
938
+
939
+ return (
940
+ <Editor.Provider state={editorState}>
941
+ <Modal>
942
+ <Editor.Input />
943
+ <Footer>
944
+ <SaveButton /> {/* Can access state */}
945
+ </Footer>
946
+ </Modal>
947
+ </Editor.Provider>
948
+ );
949
+ }
950
+ ```
951
+
952
+ ### Mistake: Overusing useEffect
953
+
954
+ ```tsx
955
+ // L WRONG - Effect for derived data
956
+ function OrderSummary({ order }: { order: Order }) {
957
+ const [total, setTotal] = useState(0);
958
+
959
+ useEffect(() => {
960
+ setTotal(order.items.reduce((sum, item) => sum + item.price, 0));
961
+ }, [order]);
962
+
963
+ return <div>Total: ${total}</div>;
964
+ }
965
+
966
+ //  CORRECT - Calculate during render
967
+ function OrderSummary({ order }: { order: Order }) {
968
+ const total = order.items.reduce((sum, item) => sum + item.price, 0);
969
+ return <div>Total: ${total}</div>;
970
+ }
971
+ ```
972
+
973
+ ## Summary
974
+
975
+ Build React applications that:
976
+
977
+ - **Compose** simple components into complex features
978
+ - **Avoid** configuration through boolean props
979
+ - **Lift** state to appropriate levels
980
+ - **Use** Effect Atom for reactive state management
981
+ - **Organize** components in namespaces like Effect modules
982
+ - **Minimize** `useEffect` usage in favor of direct calculation
983
+ - **Handle** errors explicitly with AsyncResult types
984
+ - **Test** components in isolation
985
+
986
+ This approach creates flexible, maintainable UIs that integrate seamlessly with Effect's ecosystem while following React best practices.