References
Some dependencies should always be available without forcing every caller to
provide them — a feature flag, a default page size, a logger that falls back to
the console. Context.Reference defines a service with a lazily computed
default value. Unlike Context.Service, a reference never shows up as an
unsatisfied requirement: if nobody overrides it, the default is used.
import { Context } from "effect"
// A reference holds a value and a default. The default is computed lazily the// first time the reference is read, then cached.export const FeatureFlags = Context.Reference<{ readonly betaCheckout: boolean readonly darkMode: boolean}>("myapp/FeatureFlags", { defaultValue: () => ({ betaCheckout: false, darkMode: false })})Reading a reference is identical to reading any other service — yield* it —
but the effect’s requirement type stays never, because the default guarantees
a value is always present:
import { Effect } from "effect"import { FeatureFlags } from "./FeatureFlags.ts"
// Note the requirement is `never`: this program runs without providing anything.const program: Effect.Effect<void> = Effect.gen(function*() { const flags = yield* FeatureFlags if (flags.betaCheckout) { yield* Effect.log("Using beta checkout flow") } else { yield* Effect.log("Using stable checkout flow") }})
// Runs as-is — the default `{ betaCheckout: false, darkMode: false }` is used.Effect.runFork(program)Overriding a reference
Section titled “Overriding a reference”You override a reference exactly like a normal service: provide a different value for the scope of an effect. Anything inside that scope sees the override; anything outside falls back to the default.
import { Effect } from "effect"import { FeatureFlags } from "./FeatureFlags.ts"
const withBeta = program.pipe( // `provideService` swaps in a new value for the duration of this effect. Effect.provideService(FeatureFlags, { betaCheckout: true, darkMode: true }))
Effect.runFork(withBeta) // logs "Using beta checkout flow"Because a reference is a service key, you can also override it with a layer
(Layer.succeed(FeatureFlags, { ... })) when the value should be assembled
alongside the rest of your application’s layers.
Reference vs. Service
Section titled “Reference vs. Service”Context.Service | Context.Reference | |
|---|---|---|
| Default value | none | required (defaultValue) |
| Missing at runtime | compile error / must provide | uses the default |
| Appears in requirements | yes | no |
| Typical use | database, HTTP client, repositories | flags, config defaults, tunables |
Reach for a reference when omitting the dependency should be valid and have a sensible fallback. Reach for a Service when a missing implementation should stop the program from compiling.
Defaults backed by configuration
Section titled “Defaults backed by configuration”A common pattern is a reference whose default is overridden from Configuration at startup. The reference gives you a value to read everywhere, and a layer fills it in from the environment when present:
import { Config, Context, Effect, Layer } from "effect"
// One shared default keeps the reference and the config fallback in sync.const DEFAULT_PAGE_SIZE = 20
export const PageSize = Context.Reference<number>("myapp/PageSize", { defaultValue: () => DEFAULT_PAGE_SIZE})
// Build a layer that reads PAGE_SIZE from config, falling back to the same// default the reference uses when the env var is absent.export const PageSizeFromConfig = Layer.effect( PageSize, Config.number("PAGE_SIZE").pipe(Config.withDefault(DEFAULT_PAGE_SIZE)))Provide PageSizeFromConfig to read from the environment; omit it and every
reader still gets 20. See Layers from config
for choosing whole implementations based on configuration.