Claude-skill-registry effect-patterns
Effect-TS pattern reference for TMNL. Invoke when implementing services, schemas, atoms, or Effect-based architecture. Provides canonical file locations and pattern precedents.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/effect-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-effect-patterns && rm -rf "$T"
skills/data/effect-patterns/SKILL.mdEffect-TS Patterns for TMNL
CRITICAL DOCTRINE: Atom-as-State
NO EFFECT.REF. EVER.
When React is the consumer via effect-atom,
Atom.make() is the primary state mechanism—not Effect.Ref inside services.
- Service methods mutate Atoms directly (
)Atom.set - React subscribes directly to atoms
- This eliminates the Ref→Atom bridge: no polling, no SubscriptionRef, no streams-to-consume-streams
Canonical Sources
Effect-TS Core Documentation
- Submodule:
(from packages/tmnl)../../submodules/effect/ - Website docs:
(human-authored, battle-tested)../../submodules/website/ - Test patterns:
../../submodules/effect/packages/*/test/*.test.ts
effect-atom Documentation
- Submodule:
../../submodules/effect-atom/ - Test patterns:
../../submodules/effect-atom/packages/atom/test/*.test.ts
TMNL Implementations
- Slider system:
(Effect.Service + Atom.runtime)src/lib/slider/ - Data Manager:
(canonical service pattern)src/lib/data-manager/v1/ - Layer system:
(legacy, being migrated)src/lib/layers/ - Pattern registry:
.edin/EFFECT_PATTERNS.md
Pattern Lookup Protocol
- Check submodules first - Real code beats documentation
- Check TMNL implementations - Battle-tested patterns in context
- Check .edin/EFFECT_PATTERNS.md - Curated registry
- Query deepwiki - Ask "Effect-TS/effect" for verification
Service Definition Patterns (Three Approaches)
Effect-TS provides three primary patterns for defining services. Each has specific use cases.
Decision Tree
Need a service? │ ├─ Multiple swappable implementations (Strategy Pattern)? │ └─ Use: class extends Context.Tag │ (e.g., SliderBehavior with 5 curve types) │ ├─ Effectful construction or service dependencies? │ └─ Use: class extends Effect.Service<>() │ (Default choice for most services) │ └─ Simple configuration tag? └─ Use: class extends Context.Tag with Static Default + Custom factories
Pattern 1: Effect.Service<>() — RECOMMENDED DEFAULT
When: Default choice for all services. Auto-layers, clean DI, effectful construction.
import * as Effect from 'effect/Effect' class MyService extends Effect.Service<MyService>()("app/MyService", { effect: Effect.gen(function* () { // Yield dependencies const config = yield* ConfigService; // Define methods const doThing = (input: string): Effect.Effect<number> => Effect.succeed(input.length); return { doThing } as const; }), dependencies: [ConfigService.Default], // Optional: auto-provides }) {} // Auto-generated: MyService.Default layer // Usage: yield* MyService in Effect.gen
Key Features:
- Double
syntax: first parameterizes type, second configures service()()
auto-provides required layersdependencies: [...]
ensures readonly interfaceas const
TMNL Examples:
—DataManagersrc/lib/data-manager/v1/DataManager.ts:73
—SearchKernelsrc/lib/data-manager/v1/kernels/SearchKernel.ts:308
—IdGeneratorsrc/lib/layers/v1/services/IdGenerator.ts:34
Pattern 2: class extends Context.Tag — STRATEGY PATTERN
When: Multiple swappable implementations of same interface. Runtime behavior swapping.
import * as Context from 'effect/Context' import * as Layer from 'effect/Layer' // Interface shape interface BehaviorShape { readonly id: string; readonly transform: (value: number) => number; } // Tag definition class MyBehavior extends Context.Tag('app/MyBehavior')< MyBehavior, BehaviorShape >() {} // Multiple implementations const linearImpl: BehaviorShape = { id: 'linear', transform: (v) => v, }; const logImpl: BehaviorShape = { id: 'logarithmic', transform: (v) => Math.log(v), }; // Layer factories export const LinearBehavior = { Default: Layer.succeed(MyBehavior, linearImpl), shape: linearImpl, // Direct access without Layer }; export const LogBehavior = { Default: Layer.succeed(MyBehavior, logImpl), shape: logImpl, }; // Usage: swap at runtime via Layer substitution Effect.provide(LinearBehavior.Default) // or LogBehavior.Default
Key Features:
- Export both
(Layer) AND.Default
(direct access).shape - Perfect for Strategy Pattern
- Runtime swappable via layer substitution
TMNL Examples:
—SliderBehavior
(5 behavior variants)src/lib/slider/v1/services/SliderBehavior.ts:15
Pattern 3: Context.Tag with Config — PARAMETERIZED SERVICE
When: Service needs configuration injection. Separate config tag from service.
import * as Context from 'effect/Context' import * as Layer from 'effect/Layer' // Config tag FIRST (avoid circular deps) class MyConfig extends Context.Tag('app/MyConfig')< MyConfig, { strategy: 'fast' | 'secure' } >() { static Default = Layer.succeed(this, { strategy: 'fast' }); static Custom = (config: { strategy: 'fast' | 'secure' }) => Layer.succeed(this, config); } // Service depends on config class MyService extends Effect.Service<MyService>()("app/MyService", { effect: Effect.gen(function* () { const config = yield* MyConfig; // Dependency const execute = () => config.strategy === 'fast' ? Effect.succeed('fast') : Effect.sleep('1 second').pipe(Effect.as('secure')); return { execute } as const; }), dependencies: [MyConfig.Default], }) {} // Override config at composition site const customLayer = MyService.Default.pipe( Layer.provide(MyConfig.Custom({ strategy: 'secure' })) );
TMNL Examples:
—IdGeneratorConfig + IdGeneratorsrc/lib/layers/v1/services/IdGenerator.ts
Pattern Comparison Table
| Feature | | |
|---|---|---|
| Auto-generates Layer | ✅ Yes () | ❌ Manual |
| Effectful construction | ✅ | ⚠️ Needs |
| Dependencies array | ✅ | ⚠️ Manual |
| Multiple implementations | ⚠️ Possible but awkward | ✅ Idiomatic |
| Recommended for new code | ✅ Default choice | ⚠️ Strategy Pattern only |
Common Gotchas
1. Double
Syntax()()
// WRONG class MyService extends Effect.Service<MyService>("id", { ... }) {} // CORRECT class MyService extends Effect.Service<MyService>()("id", { ... }) {}
2. Config Tag BEFORE Service
// WRONG — Circular dependency! class MyService extends Effect.Service<MyService>()("id", { effect: Effect.gen(function* () { const config = yield* MyConfig; // MyConfig not defined yet! }), }) {} class MyConfig extends Context.Tag("config")<...>() {} // CORRECT — Config first class MyConfig extends Context.Tag("config")<...>() {} class MyService extends Effect.Service<MyService>()("id", { ... }) {}
3. Always use as const
// WRONG return { doThing }; // CORRECT return { doThing } as const;
Atom-as-State (THE State Pattern)
When React consumes Effect services, Atoms ARE the state.
import { Atom } from '@effect-rx/rx-react' import { Effect, Layer } from 'effect' // State lives in Atoms, NOT Refs const resultsAtom = Atom.make<SearchResult[]>([]) const statusAtom = Atom.make<'idle' | 'loading' | 'complete'>('idle') // Service methods mutate Atoms directly interface SearchService { readonly search: (query: string) => Effect.Effect<void> } const searchServiceImpl: SearchService = { search: (query) => Effect.gen(function* () { Atom.set(statusAtom, 'loading') Atom.set(resultsAtom, []) const results = yield* performSearch(query) Atom.set(resultsAtom, results) Atom.set(statusAtom, 'complete') }) } // React subscribes directly function SearchResults() { const results = useAtomValue(resultsAtom) const status = useAtomValue(statusAtom) return <Grid data={results} loading={status === 'loading'} /> }
Canonical source:
src/lib/data-manager/v1/DataManager.ts
Pattern 3: Atom.runtime for Service Composition
Combine service layers with reactive atoms.
import { Atom } from '@effect-rx/rx-react' import { Layer } from 'effect' // Create runtime from composed layers export const dataManagerRuntime = Atom.runtime( Layer.mergeAll( SearchKernel.Live, StreamService.Live, DataManager.Live ) ) // Create operation atoms export const searchAtom = dataManagerRuntime.fn( (query: string) => Effect.gen(function* () { const dm = yield* DataManager yield* dm.search(query) }) ) // React usage function SearchBox() { const doSearch = useAtomCallback(searchAtom) return <input onChange={e => doSearch(e.target.value)} /> }
Canonical source:
src/lib/slider/atoms/index.ts
Pattern 4: Schema.TaggedStruct for Events
All domain events use discriminated unions with
_tag.
import { Schema } from 'effect' const SearchStarted = Schema.TaggedStruct('SearchStarted', { query: Schema.String, timestamp: Schema.DateFromSelf, }) const SearchCompleted = Schema.TaggedStruct('SearchCompleted', { query: Schema.String, resultCount: Schema.Number, durationMs: Schema.Number, }) const SearchEvent = Schema.Union(SearchStarted, SearchCompleted) type SearchEvent = typeof SearchEvent.Type // Pattern match on _tag function handle(event: SearchEvent) { switch (event._tag) { case 'SearchStarted': return startSpinner() case 'SearchCompleted': return showResults(event.resultCount) } }
Canonical source:
src/lib/data-manager/v1/types.ts
Pattern 5: Schema.TaggedClass for Entities
Entities with methods use TaggedClass.
import { Schema } from 'effect' class GridColumn extends Schema.TaggedClass<GridColumn>()('GridColumn', { field: Schema.String, headerName: Schema.String, width: Schema.optional(Schema.Number), sortable: Schema.optional(Schema.Boolean), }) { get displayWidth() { return this.width ?? 150 } withWidth(width: number) { return new GridColumn({ ...this, width }) } }
Canonical source:
src/lib/data-grid/types.ts
Pattern 6: Effect.withSpan for Observability
Traced operations for DevTools visibility.
const search = (query: string) => Effect.gen(function* () { yield* Effect.log(`Searching: ${query}`) const results = yield* searchKernel.query(query) return results }).pipe( Effect.withSpan('DataManager.search', { attributes: { query, timestamp: Date.now() } }) )
Canonical source:
src/lib/data-manager/v1/DataManager.ts
Pattern 7: Layer Composition
Compose layers for dependency injection.
// Individual service layers const IdGeneratorLive = Layer.succeed(IdGenerator, nanoidGenerator) const FactoryLive = Layer.effect(Factory, makeFactory).pipe( Layer.provide(IdGeneratorLive) ) const ManagerLive = Layer.effect(Manager, makeManager).pipe( Layer.provide(FactoryLive) ) // Compose into single runtime layer const AppLayer = Layer.mergeAll( IdGeneratorLive, FactoryLive, ManagerLive ) // Use with Atom.runtime const appRuntime = Atom.runtime(AppLayer)
Canonical source:
src/lib/layers/index.ts
Pattern 8: Error Handling with Tagged Errors
Domain errors as tagged structs.
import { Schema, Data } from 'effect' class SearchError extends Data.TaggedError('SearchError')<{ readonly query: string readonly cause: unknown }> {} class IndexNotReadyError extends Data.TaggedError('IndexNotReadyError')<{ readonly kernel: string }> {} // Usage const search = (query: string) => Effect.gen(function* () { if (!indexed) { yield* Effect.fail(new IndexNotReadyError({ kernel: 'flex' })) } // ... }) // Pattern match errors Effect.catchTags({ SearchError: (e) => Effect.log(`Search failed: ${e.query}`), IndexNotReadyError: (e) => Effect.log(`Index ${e.kernel} not ready`), })
Filing New Patterns
When you discover or create a new Effect pattern:
- Implement in TMNL first - Working code in
src/lib/ - Add to registry - Update
.edin/EFFECT_PATTERNS.md - Update this skill - Add pattern with canonical source
- Create bead - Track with
bd create --type=task --title="Document X pattern"
Anti-Patterns (BANNED)
Effect.Ref for React State
// BANNED - Do not do this const stateRef = yield* Ref.make<State>(initial) // ...poll ref, bridge to atom, complexity explosion
Streams-to-Consume-Streams
// BANNED - SubscriptionRef → Stream → consume → update atom // Just use Atom.set directly in service methods
useState for Cross-Component State
// BANNED when state crosses boundaries const [results, setResults] = useState([]) const [status, setStatus] = useState('idle') // Use Atom.make + service methods instead
Related Ecosystem
- Agent:
(TODO).claude/agents/effect-specialist.md - Command:
(TODO).claude/commands/effect.md - Hook:
(TODO).claude/hooks/effect-patterns.json