effect-best-practices by makisuo/skills
npx skills add https://github.com/makisuo/skills --skill effect-best-practices本技能为 Effect-TS 代码库强制执行一套有主见的、一致的规范。这些规范旨在优化类型安全、可测试性、可观测性和可维护性。
Effect 语言服务器对于 Effect 开发至关重要。 它能捕获仅靠 TypeScript 无法检测的编辑时错误,提供 Effect 特定的重构功能,并提高开发效率。
npm install @effect/language-service --save-dev
tsconfig.json:{
"compilerOptions": {
"plugins": [{ "name": "@effect/language-service" }]
}
}
3. 配置你的编辑器以使用工作区 TypeScript 版本: * VSCode : 按 F1 → 输入 "TypeScript: Select TypeScript Version" → 选择 "Use Workspace Version" * JetBrains : 设置 → 语言与框架 → TypeScript → 使用工作区版本
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
Self、Duration 字符串、Schema 品牌类型用于 CI 强制执行:
npx effect-language-service patch
有关配置选项和 CLI 工具,请参阅 references/language-server.md。
| 类别 | 推荐做法 | 避免做法 |
|---|---|---|
| 服务 | 使用 Effect.Service 并设置 accessors: true | 为业务逻辑使用 Context.Tag |
| 依赖 | 在服务中使用 dependencies: [Dep.Default] | 在使用处手动 Layer.provide |
| 层 | 使用 Layer.mergeAll 进行扁平化组合 | 深度嵌套的 Layer.provide 链 |
| 层链式调用 | 使用 Layer.provideMerge 进行增量组合 | 多次使用 Layer.provide(会产生嵌套类型) |
| 错误 | 使用带 message 字段的 Schema.TaggedError | 使用普通类或通用 Error |
| 错误特异性 | 使用 UserNotFoundError、SessionExpiredError | 使用通用的 NotFoundError、BadRequestError |
| 错误处理 | 使用 catchTag/catchTags | 使用 catchAll 或 mapError |
| ID | 使用 Schema.UUID.pipe(Schema.brand("@App/EntityId")) | 为实体 ID 使用普通 string |
| 函数 | 使用 Effect.fn("Service.method") | 使用匿名生成器 |
| 日志 | 使用带结构化数据的 Effect.log | 使用 console.log |
| 配置 | 使用带验证的 Config.* | 直接使用 process.env |
| 选项 | 使用包含两种情况的 Option.match | 使用 Option.getOrThrow |
| 可空性 | 在领域类型中使用 Option<T> | 使用 null/undefined |
| 原子 | 在组件外部使用 Atom.make | 在渲染函数内部创建原子 |
| 原子状态 | 为全局状态使用 Atom.keepAlive | 为持久化状态忘记使用 keepAlive |
| 原子更新 | 在 React 组件中使用 useAtomSet | 从 React 中命令式地使用 Atom.update |
| 原子清理 | 为副作用使用 get.addFinalizer() | 为事件监听器缺失清理 |
| 原子结果 | 使用带 onErrorTag 的 Result.builder | 忽略加载/错误状态 |
始终为业务逻辑服务使用 Effect.Service。这提供了自动访问器、内置的 Default 层和正确的依赖声明。
import { Effect } from "effect"
export class UserService extends Effect.Service<UserService>()("UserService", {
accessors: true,
dependencies: [UserRepo.Default, CacheService.Default],
effect: Effect.gen(function* () {
const repo = yield* UserRepo
const cache = yield* CacheService
const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
const cached = yield* cache.get(id)
if (Option.isSome(cached)) return cached.value
const user = yield* repo.findById(id)
yield* cache.set(id, user)
return user
})
const create = Effect.fn("UserService.create")(function* (data: CreateUserInput) {
const user = yield* repo.create(data)
yield* Effect.log("User created", { userId: user.id })
return user
})
return { findById, create }
}),
}) {}
// 使用 - 依赖已自动装配
const program = Effect.gen(function* () {
const user = yield* UserService.findById(userId)
return user
})
// 在应用根目录
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default)
在以下情况下可以使用 Context.Tag:
详细模式请参阅 references/service-patterns.md。
始终为错误使用 Schema.TaggedError。这使它们可序列化(RPC 所需)并提供一致的结构。
import { Schema } from "effect"
import { HttpApiSchema } from "@effect/platform"
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{
userId: UserId,
message: Schema.String,
},
HttpApiSchema.annotations({ status: 404 }),
) {}
export class UserCreateError extends Schema.TaggedError<UserCreateError>()(
"UserCreateError",
{
message: Schema.String,
cause: Schema.optional(Schema.String),
},
HttpApiSchema.annotations({ status: 400 }),
) {}
错误处理 - 使用 catchTag/catchTags:
// 正确 - 保留类型信息
yield* repo.findById(id).pipe(
Effect.catchTag("DatabaseError", (err) =>
Effect.fail(new UserNotFoundError({ userId: id, message: "Lookup failed" }))
),
Effect.catchTag("ConnectionError", (err) =>
Effect.fail(new ServiceUnavailableError({ message: "Database unreachable" }))
),
)
// 正确 - 一次处理多个标签
yield* effect.pipe(
Effect.catchTags({
DatabaseError: (err) => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })),
ValidationError: (err) => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message })),
}),
)
每个不同的失败原因都应拥有自己的错误类型。 不要将多种失败模式折叠成通用的 HTTP 错误。
// 错误 - 通用错误会丢失信息
export class NotFoundError extends Schema.TaggedError<NotFoundError>()(
"NotFoundError",
{ message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
// 然后将所有错误映射到它:
Effect.catchTags({
UserNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
ChannelNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
MessageNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
})
// 前端得到无用的信息:{ _tag: "NotFoundError", message: "Not found" }
// 哪个资源?用户?频道?消息?无法区分!
// 正确 - 具有丰富上下文的显式领域错误
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{ userId: UserId, message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
export class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()(
"ChannelNotFoundError",
{ channelId: ChannelId, message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
export class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()(
"SessionExpiredError",
{ sessionId: SessionId, expiredAt: Schema.DateTimeUtc, message: Schema.String },
HttpApiSchema.annotations({ status: 401 }),
) {}
// 前端现在可以显示特定的 UI:
// - UserNotFoundError → "用户不存在"
// - ChannelNotFoundError → "频道已被删除"
// - SessionExpiredError → "您的会话已过期。请重新登录。"
有关错误重映射和重试模式,请参阅 references/error-patterns.md。
为所有实体 ID 添加品牌,以实现跨服务边界的类型安全:
import { Schema } from "effect"
// 实体 ID - 始终添加品牌
export const UserId = Schema.UUID.pipe(Schema.brand("@App/UserId"))
export type UserId = Schema.Schema.Type<typeof UserId>
export const OrganizationId = Schema.UUID.pipe(Schema.brand("@App/OrganizationId"))
export type OrganizationId = Schema.Schema.Type<typeof OrganizationId>
// 领域类型 - 使用 Schema.Struct
export const User = Schema.Struct({
id: UserId,
email: Schema.String,
name: Schema.String,
organizationId: OrganizationId,
createdAt: Schema.DateTimeUtc,
})
export type User = Schema.Schema.Type<typeof User>
// 用于变更的输入类型
export const CreateUserInput = Schema.Struct({
email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
name: Schema.String.pipe(Schema.minLength(1)),
organizationId: OrganizationId,
})
export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>
何时不应添加品牌:
有关转换和高级模式,请参阅 references/schema-patterns.md。
始终为服务方法使用 Effect.fn。这提供了具有适当跨度名称的自动追踪:
// 正确 - 使用描述性名称的 Effect.fn
const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
yield* Effect.annotateCurrentSpan("userId", id)
const user = yield* repo.findById(id)
return user
})
// 正确 - 带多个参数的 Effect.fn
const transfer = Effect.fn("AccountService.transfer")(
function* (fromId: AccountId, toId: AccountId, amount: number) {
yield* Effect.annotateCurrentSpan("fromId", fromId)
yield* Effect.annotateCurrentSpan("toId", toId)
yield* Effect.annotateCurrentSpan("amount", amount)
// ...
}
)
在服务中声明依赖,而不是在使用处:
// 正确 - 在服务定义中声明依赖
export class OrderService extends Effect.Service<OrderService>()("OrderService", {
accessors: true,
dependencies: [
UserService.Default,
ProductService.Default,
PaymentService.Default,
],
effect: Effect.gen(function* () {
const users = yield* UserService
const products = yield* ProductService
const payments = yield* PaymentService
// ...
}),
}) {}
// 在应用根目录 - 简单合并
const AppLive = Layer.mergeAll(
OrderService.Default,
// 基础设施层(有意不放在依赖中)
DatabaseLive,
RedisLive,
)
层组合模式:
// 对同一级别的层使用 Layer.mergeAll 进行扁平化组合
const RepoLive = Layer.mergeAll(
UserRepo.Default,
OrderRepo.Default,
ProductRepo.Default,
)
// 使用 Layer.provideMerge 进行增量链式调用(比 Layer.provide 产生更扁平的类型)
const MainLive = DatabaseLive.pipe(
Layer.provideMerge(ConfigServiceLive),
Layer.provideMerge(LoggerLive),
Layer.provideMerge(CacheLive),
)
为何使用层而非 Effect.provide:
Effect.provide 每次调用都会创建新实例。Layer.provide 会产生复杂的递归类型,拖慢 LSP。Layer.mergeAll 和 Layer.provideMerge 产生更扁平的类型。有关测试层、配置依赖层和 layerConfig 模式,请参阅 references/layer-patterns.md。
切勿使用 Option.getOrThrow。始终显式处理两种情况:
// 正确 - 显式处理
yield* Option.match(maybeUser, {
onNone: () => Effect.fail(new UserNotFoundError({ userId, message: "Not found" })),
onSome: (user) => Effect.succeed(user),
})
// 正确 - 使用 getOrElse 设置默认值
const name = Option.getOrElse(maybeName, () => "Anonymous")
// 正确 - 使用 Option.map 进行转换
const upperName = Option.map(maybeName, (n) => n.toUpperCase())
Effect Atom 为 React 提供了与 Effect 集成的响应式状态管理。
import { Atom } from "@effect-atom/atom-react"
// 在组件外部定义原子
const countAtom = Atom.make(0)
// 为需要持久化的全局状态使用 keepAlive
const userPrefsAtom = Atom.make({ theme: "dark" }).pipe(Atom.keepAlive)
// 原子族用于每个实体的状态
const modalAtomFamily = Atom.family((type: string) =>
Atom.make({ isOpen: false }).pipe(Atom.keepAlive)
)
import { useAtomValue, useAtomSet, useAtom, useAtomMount } from "@effect-atom/atom-react"
function Counter() {
const count = useAtomValue(countAtom) // 只读
const setCount = useAtomSet(countAtom) // 只写
const [value, setValue] = useAtom(countAtom) // 读写
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// 挂载副作用原子而不读取值
function App() {
useAtomMount(keyboardShortcutsAtom)
return <>{children}</>
}
使用 Result.builder 来渲染具有副作用的原子结果。它提供了带有 onErrorTag 的可链式错误处理:
import { Result } from "@effect-atom/atom-react"
function UserProfile() {
const userResult = useAtomValue(userAtom) // Result<User, Error>
return Result.builder(userResult)
.onInitial(() => <div>Loading...</div>)
.onErrorTag("NotFoundError", () => <div>User not found</div>)
.onError((error) => <div>Error: {error.message}</div>)
.onSuccess((user) => <div>Hello, {user.name}</div>)
.render()
}
const scrollYAtom = Atom.make((get) => {
const onScroll = () => get.setSelf(window.scrollY)
window.addEventListener("scroll", onScroll)
get.addFinalizer(() => window.removeEventListener("scroll", onScroll)) // 必需
return window.scrollY
}).pipe(Atom.keepAlive)
对于变更原子,从 result.waiting 派生加载状态,而不是使用 useState:
const [result, mutate] = useAtom(deleteMutation, { mode: "promise" })
const isLoading = result.waiting // 自动更新,无需 useState/finally
对话框所有权: 将变更逻辑移入对话框组件。对话框拥有变更钩子、加载状态和提示。父组件提供数据属性和一个 onSuccess 回调。
缓存失效: 在变更和查询原子上都使用 reactivityKeys,以便在变更后自动使查询失效——替代手动的 refresh() 调用。
有关原子族、localStorage、变更和反模式的完整模式,请参阅 references/effect-atom-patterns.md。
关于 RPC 契约和集群工作流,请参阅:
references/rpc-cluster-patterns.md - RpcGroup、Workflow.make、Activity 模式以下模式绝不可接受:
// 禁止 - 在服务内部使用 runSync/runPromise
const result = Effect.runSync(someEffect) // 永远不要这样做
// 禁止 - 在 Effect.gen 内部抛出错误
yield* Effect.gen(function* () {
if (bad) throw new Error("No!") // 改用 Effect.fail
})
// 禁止 - 使用 catchAll 丢失类型信息
yield* effect.pipe(Effect.catchAll(() => Effect.fail(new GenericError())))
// 禁止 - 使用 console.log
console.log("debug") // 改用 Effect.log
// 禁止 - 直接使用 process.env
const key = process.env.API_KEY // 改用 Config.string("API_KEY")
// 禁止 - 在领域类型中使用 null/undefined
type User = { name: string | null } // 改用 Option<string>
有关完整列表及其理由,请参阅 references/anti-patterns.md。
// 结构化日志
yield* Effect.log("Processing order", { orderId, userId, amount })
// 指标
const orderCounter = Metric.counter("orders_processed")
yield* Metric.increment(orderCounter)
// 带验证的配置
const config = Config.all({
port: Config.integer("PORT").pipe(Config.withDefault(3000)),
apiKey: Config.redacted("API_KEY"),
maxRetries: Config.integer("MAX_RETRIES").pipe(
Config.validate({ message: "Must be positive", validation: (n) => n > 0 })
),
})
有关指标和追踪模式,请参阅 references/observability-patterns.md。
有关详细模式,请查阅 references/ 目录中的这些参考文件:
language-server.md - Effect 语言服务设置、诊断、重构、CLI 工具service-patterns.md - 服务定义、Effect.fn、Context.Tag 例外情况error-patterns.md - Schema.TaggedError、错误重映射、重试模式schema-patterns.md - 品牌类型、转换、Schema.Classlayer-patterns.md - 依赖组合、测试层rpc-cluster-patterns.md - RpcGroup、Workflow、Activity 模式effect-atom-patterns.md - Atom、原子族、React 钩子、结果处理anti-patterns.md - 禁止模式的完整列表observability-patterns.md - 日志、指标、配置模式每周安装量
490
仓库
GitHub 星标
30
首次出现
2026年1月25日
安全审计
安装于
opencode414
codex389
gemini-cli358
github-copilot356
claude-code328
cursor321
This skill enforces opinionated, consistent patterns for Effect-TS codebases. These patterns optimize for type safety, testability, observability, and maintainability.
The Effect Language Server is essential for Effect development. It catches errors at edit-time that TypeScript alone cannot detect, provides Effect-specific refactors, and improves developer productivity.
npm install @effect/language-service --save-dev
tsconfig.json:{
"compilerOptions": {
"plugins": [{ "name": "@effect/language-service" }]
}
}
3. Configure your editor to use workspace TypeScript: * VSCode : F1 → "TypeScript: Select TypeScript Version" → "Use Workspace Version" * JetBrains : Settings → Languages & Frameworks → TypeScript → Use workspace version
Self, Duration strings, Schema brandsFor CI enforcement:
npx effect-language-service patch
See references/language-server.md for configuration options and CLI tools.
| Category | DO | DON'T |
|---|---|---|
| Services | Effect.Service with accessors: true | Context.Tag for business logic |
| Dependencies | dependencies: [Dep.Default] in service | Manual Layer.provide at usage sites |
| Layers | Layer.mergeAll for flat composition | Deeply nested Layer.provide chains |
Always useEffect.Service for business logic services. This provides automatic accessors, built-in Default layer, and proper dependency declaration.
import { Effect } from "effect"
export class UserService extends Effect.Service<UserService>()("UserService", {
accessors: true,
dependencies: [UserRepo.Default, CacheService.Default],
effect: Effect.gen(function* () {
const repo = yield* UserRepo
const cache = yield* CacheService
const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
const cached = yield* cache.get(id)
if (Option.isSome(cached)) return cached.value
const user = yield* repo.findById(id)
yield* cache.set(id, user)
return user
})
const create = Effect.fn("UserService.create")(function* (data: CreateUserInput) {
const user = yield* repo.create(data)
yield* Effect.log("User created", { userId: user.id })
return user
})
return { findById, create }
}),
}) {}
// Usage - dependencies are already wired
const program = Effect.gen(function* () {
const user = yield* UserService.findById(userId)
return user
})
// At app root
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default)
WhenContext.Tag is acceptable:
See references/service-patterns.md for detailed patterns.
Always useSchema.TaggedError for errors. This makes them serializable (required for RPC) and provides consistent structure.
import { Schema } from "effect"
import { HttpApiSchema } from "@effect/platform"
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{
userId: UserId,
message: Schema.String,
},
HttpApiSchema.annotations({ status: 404 }),
) {}
export class UserCreateError extends Schema.TaggedError<UserCreateError>()(
"UserCreateError",
{
message: Schema.String,
cause: Schema.optional(Schema.String),
},
HttpApiSchema.annotations({ status: 400 }),
) {}
Error handling - usecatchTag/catchTags:
// CORRECT - preserves type information
yield* repo.findById(id).pipe(
Effect.catchTag("DatabaseError", (err) =>
Effect.fail(new UserNotFoundError({ userId: id, message: "Lookup failed" }))
),
Effect.catchTag("ConnectionError", (err) =>
Effect.fail(new ServiceUnavailableError({ message: "Database unreachable" }))
),
)
// CORRECT - multiple tags at once
yield* effect.pipe(
Effect.catchTags({
DatabaseError: (err) => Effect.fail(new UserNotFoundError({ userId: id, message: err.message })),
ValidationError: (err) => Effect.fail(new InvalidEmailError({ email: input.email, message: err.message })),
}),
)
Every distinct failure reason deserves its own error type. Don't collapse multiple failure modes into generic HTTP errors.
// WRONG - Generic errors lose information
export class NotFoundError extends Schema.TaggedError<NotFoundError>()(
"NotFoundError",
{ message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
// Then mapping everything to it:
Effect.catchTags({
UserNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
ChannelNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
MessageNotFoundError: (err) => Effect.fail(new NotFoundError({ message: "Not found" })),
})
// Frontend gets useless: { _tag: "NotFoundError", message: "Not found" }
// Which resource? User? Channel? Message? Can't tell!
// CORRECT - Explicit domain errors with rich context
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{ userId: UserId, message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
export class ChannelNotFoundError extends Schema.TaggedError<ChannelNotFoundError>()(
"ChannelNotFoundError",
{ channelId: ChannelId, message: Schema.String },
HttpApiSchema.annotations({ status: 404 }),
) {}
export class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()(
"SessionExpiredError",
{ sessionId: SessionId, expiredAt: Schema.DateTimeUtc, message: Schema.String },
HttpApiSchema.annotations({ status: 401 }),
) {}
// Frontend can now show specific UI:
// - UserNotFoundError → "User doesn't exist"
// - ChannelNotFoundError → "Channel was deleted"
// - SessionExpiredError → "Your session expired. Please log in again."
See references/error-patterns.md for error remapping and retry patterns.
Brand all entity IDs for type safety across service boundaries:
import { Schema } from "effect"
// Entity IDs - always branded
export const UserId = Schema.UUID.pipe(Schema.brand("@App/UserId"))
export type UserId = Schema.Schema.Type<typeof UserId>
export const OrganizationId = Schema.UUID.pipe(Schema.brand("@App/OrganizationId"))
export type OrganizationId = Schema.Schema.Type<typeof OrganizationId>
// Domain types - use Schema.Struct
export const User = Schema.Struct({
id: UserId,
email: Schema.String,
name: Schema.String,
organizationId: OrganizationId,
createdAt: Schema.DateTimeUtc,
})
export type User = Schema.Schema.Type<typeof User>
// Input types for mutations
export const CreateUserInput = Schema.Struct({
email: Schema.String.pipe(Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)),
name: Schema.String.pipe(Schema.minLength(1)),
organizationId: OrganizationId,
})
export type CreateUserInput = Schema.Schema.Type<typeof CreateUserInput>
When NOT to brand:
See references/schema-patterns.md for transforms and advanced patterns.
Always useEffect.fn for service methods. This provides automatic tracing with proper span names:
// CORRECT - Effect.fn with descriptive name
const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
yield* Effect.annotateCurrentSpan("userId", id)
const user = yield* repo.findById(id)
return user
})
// CORRECT - Effect.fn with multiple parameters
const transfer = Effect.fn("AccountService.transfer")(
function* (fromId: AccountId, toId: AccountId, amount: number) {
yield* Effect.annotateCurrentSpan("fromId", fromId)
yield* Effect.annotateCurrentSpan("toId", toId)
yield* Effect.annotateCurrentSpan("amount", amount)
// ...
}
)
Declare dependencies in the service , not at usage sites:
// CORRECT - dependencies in service definition
export class OrderService extends Effect.Service<OrderService>()("OrderService", {
accessors: true,
dependencies: [
UserService.Default,
ProductService.Default,
PaymentService.Default,
],
effect: Effect.gen(function* () {
const users = yield* UserService
const products = yield* ProductService
const payments = yield* PaymentService
// ...
}),
}) {}
// At app root - simple merge
const AppLive = Layer.mergeAll(
OrderService.Default,
// Infrastructure layers (intentionally not in dependencies)
DatabaseLive,
RedisLive,
)
Layer composition patterns:
// Use Layer.mergeAll for flat composition of same-level layers
const RepoLive = Layer.mergeAll(
UserRepo.Default,
OrderRepo.Default,
ProductRepo.Default,
)
// Use Layer.provideMerge for incremental chaining (flatter types than Layer.provide)
const MainLive = DatabaseLive.pipe(
Layer.provideMerge(ConfigServiceLive),
Layer.provideMerge(LoggerLive),
Layer.provideMerge(CacheLive),
)
Why layers overEffect.provide:
Effect.provide creates new instances each call.Layer.provide nesting creates complex recursive types that slow the LSP. Layer.mergeAll and Layer.provideMerge produce flatter types.See references/layer-patterns.md for testing layers, config-dependent layers, and the layerConfig pattern.
Never useOption.getOrThrow. Always handle both cases explicitly:
// CORRECT - explicit handling
yield* Option.match(maybeUser, {
onNone: () => Effect.fail(new UserNotFoundError({ userId, message: "Not found" })),
onSome: (user) => Effect.succeed(user),
})
// CORRECT - with getOrElse for defaults
const name = Option.getOrElse(maybeName, () => "Anonymous")
// CORRECT - Option.map for transformations
const upperName = Option.map(maybeName, (n) => n.toUpperCase())
Effect Atom provides reactive state management for React with Effect integration.
import { Atom } from "@effect-atom/atom-react"
// Define atoms OUTSIDE components
const countAtom = Atom.make(0)
// Use keepAlive for global state that should persist
const userPrefsAtom = Atom.make({ theme: "dark" }).pipe(Atom.keepAlive)
// Atom families for per-entity state
const modalAtomFamily = Atom.family((type: string) =>
Atom.make({ isOpen: false }).pipe(Atom.keepAlive)
)
import { useAtomValue, useAtomSet, useAtom, useAtomMount } from "@effect-atom/atom-react"
function Counter() {
const count = useAtomValue(countAtom) // Read only
const setCount = useAtomSet(countAtom) // Write only
const [value, setValue] = useAtom(countAtom) // Read + write
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}
// Mount side-effect atoms without reading value
function App() {
useAtomMount(keyboardShortcutsAtom)
return <>{children}</>
}
UseResult.builder for rendering effectful atom results. It provides chainable error handling with onErrorTag:
import { Result } from "@effect-atom/atom-react"
function UserProfile() {
const userResult = useAtomValue(userAtom) // Result<User, Error>
return Result.builder(userResult)
.onInitial(() => <div>Loading...</div>)
.onErrorTag("NotFoundError", () => <div>User not found</div>)
.onError((error) => <div>Error: {error.message}</div>)
.onSuccess((user) => <div>Hello, {user.name}</div>)
.render()
}
const scrollYAtom = Atom.make((get) => {
const onScroll = () => get.setSelf(window.scrollY)
window.addEventListener("scroll", onScroll)
get.addFinalizer(() => window.removeEventListener("scroll", onScroll)) // REQUIRED
return window.scrollY
}).pipe(Atom.keepAlive)
For mutation atoms, derive loading state from result.waiting instead of useState:
const [result, mutate] = useAtom(deleteMutation, { mode: "promise" })
const isLoading = result.waiting // Updates automatically, no useState/finally needed
Dialog ownership: Move mutation logic into dialog components. Dialog owns the mutation hook, loading state, and toasts. Parent provides data props and an onSuccess callback.
Cache invalidation: Use reactivityKeys on both mutation and query atoms to auto-invalidate queries after mutations — replaces manual refresh() calls.
See references/effect-atom-patterns.md for complete patterns including families, localStorage, mutations, and anti-patterns.
For RPC contracts and cluster workflows, see:
references/rpc-cluster-patterns.md - RpcGroup, Workflow.make, Activity patternsThese patterns are never acceptable :
// FORBIDDEN - runSync/runPromise inside services
const result = Effect.runSync(someEffect) // Never do this
// FORBIDDEN - throw inside Effect.gen
yield* Effect.gen(function* () {
if (bad) throw new Error("No!") // Use Effect.fail instead
})
// FORBIDDEN - catchAll losing type info
yield* effect.pipe(Effect.catchAll(() => Effect.fail(new GenericError())))
// FORBIDDEN - console.log
console.log("debug") // Use Effect.log
// FORBIDDEN - process.env directly
const key = process.env.API_KEY // Use Config.string("API_KEY")
// FORBIDDEN - null/undefined in domain types
type User = { name: string | null } // Use Option<string>
See references/anti-patterns.md for the complete list with rationale.
// Structured logging
yield* Effect.log("Processing order", { orderId, userId, amount })
// Metrics
const orderCounter = Metric.counter("orders_processed")
yield* Metric.increment(orderCounter)
// Config with validation
const config = Config.all({
port: Config.integer("PORT").pipe(Config.withDefault(3000)),
apiKey: Config.redacted("API_KEY"),
maxRetries: Config.integer("MAX_RETRIES").pipe(
Config.validate({ message: "Must be positive", validation: (n) => n > 0 })
),
})
See references/observability-patterns.md for metrics and tracing patterns.
For detailed patterns, consult these reference files in the references/ directory:
language-server.md - Effect Language Service setup, diagnostics, refactors, CLI toolsservice-patterns.md - Service definition, Effect.fn, Context.Tag exceptionserror-patterns.md - Schema.TaggedError, error remapping, retry patternsschema-patterns.md - Branded types, transforms, Schema.Classlayer-patterns.md - Dependency composition, testing layersrpc-cluster-patterns.md - RpcGroup, Workflow, Activity patternseffect-atom-patterns.md - Atom, families, React hooks, Result handlinganti-patterns.md - Complete list of forbidden patternsobservability-patterns.md - Logging, metrics, config patternsWeekly Installs
490
Repository
GitHub Stars
30
First Seen
Jan 25, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode414
codex389
gemini-cli358
github-copilot356
claude-code328
cursor321
Vue.js测试最佳实践:Vue 3组件、组合式函数、Pinia与异步测试完整指南
3,700 周安装
| Layer Chaining | Layer.provideMerge for incremental composition | Multiple Layer.provide (creates nested types) |
| Errors | Schema.TaggedError with message field | Plain classes or generic Error |
| Error Specificity | UserNotFoundError, SessionExpiredError | Generic NotFoundError, BadRequestError |
| Error Handling | catchTag/catchTags | catchAll or mapError |
| IDs | Schema.UUID.pipe(Schema.brand("@App/EntityId")) | Plain string for entity IDs |
| Functions | Effect.fn("Service.method") | Anonymous generators |
| Logging | Effect.log with structured data | console.log |
| Config | Config.* with validation | process.env directly |
| Options | Option.match with both cases | Option.getOrThrow |
| Nullability | Option<T> in domain types | null/undefined |
| Atoms | Atom.make outside components | Creating atoms inside render |
| Atom State | Atom.keepAlive for global state | Forgetting keepAlive for persistent state |
| Atom Updates | useAtomSet in React components | Atom.update imperatively from React |
| Atom Cleanup | get.addFinalizer() for side effects | Missing cleanup for event listeners |
| Atom Results | Result.builder with onErrorTag | Ignoring loading/error states |