Skip to content

Overview

Unitflow is an Effect runtime for React.

A Unitflow model is an Effect service that encapsulates a component’s business logic. It owns state, actions, async work, child models, and lifetime. It can depend on other UI models and regular Effect services through layers. A View binds the model to the screen at the edge, and — for a model it leases itself — owns its lifecycle too: mounting leases it, unmounting releases it.

Model -> Effect service encapsulating a component's logic
Store -> model state
Event -> model action
Query -> async read state
Mutation -> async write state
View -> render binding, owns lifecycle for models it leases itself

Unitflow is an attempt at a middle point: flexible enough that logic of any complexity is easy to build out of Effect and the primitives it gives you, but not so flexible that every model ends up reinventing its own shape. Enough freedom to build anything; not enough freedom to build it ten different ways.

Effect already gives an application dependency injection, Layer, Scope, Stream, fibers, schedules, typed errors, Schema, and test-friendly service replacement. Unitflow keeps those tools available in UI state instead of building a separate UI framework.

Unitflow adds only the pieces that UI models need:

  • stores for renderable state
  • events for model actions
  • queries and mutations for visible async state
  • a registry for keyed model instances and lifetime
  • a React binding that renders a model’s ui

Those pieces are Effect-idiomatic. Stores, events, queries, mutations, and layers compose with .pipe(...), so model code reads like regular Effect code instead of a separate UI DSL.

The result is UI state management that still feels like Effect architecture.

Unitflow is inspired by Effector: explicit stores, events, and UI logic outside the component tree, rebuilt around Effect services, layers, scopes, and dependency injection.

Effect Atom is powerful, but Atom is a broad primitive: state, derived data, async work, writes, and persistence can all become atoms, and the code structure is still up to you. That much freedom is also hard on agents: without an explicit shape, generated code tends to become messy and hard to review. Unitflow is more opinionated: it gives frontend code a model-first blueprint, where the UI unit is an Effect service resolved through dependency injection and built from smaller primitives like stores, events, queries, and mutations.

The useful shape is not a giant model. It is a parent model that owns a flow, plus child models that own focused domains.

export class TaskModel extends Model.Service<TaskModel>()(
"examples/task-board/task",
)<TaskKey>()({
make: ({ id }) =>
Effect.gen(function* () {
const state = Store.make<TaskState>(initialTask(id));
const rename = yield* Event.input<string>().pipe(
Event.handler((title) =>
Store.update(state, (task) => ({
...task,
title: title.trim() === "" ? task.title : title,
})),
),
);
const move = yield* Event.input<TaskStatus>().pipe(
Event.handler((status) =>
Store.update(state, (task) => ({ ...task, status })),
),
);
return {
inputs: { rename, move },
outputs: { state },
ui: { state, rename, move },
};
}),
}) {}

TaskModel and BoardModel are services in the Effect graph. A test can provide the real child model, replace it with Model.layerValue(...), or provide fake API services that the model depends on. The View only renders the model’s ui.

  • UI models compose with Effect dependency injection.
  • Unitflow primitives compose with .pipe(...) like Effect primitives.
  • Model state and async work live in the same Effect runtime as the rest of the app.
  • Parent models can own child models or dynamic lists of child models.
  • Tests can build a small layer, drive model actions, and assert model state.
  • Lifecycle uses Effect scopes, so fibers, subscriptions, and finalizers close with the model instance.

Start with the small primitives, then move into models and async work:

Stores -> state: make, get, set, derive, changed
Events -> actions: make, emit, handler
Model -> Effect service, contract, dependencies, children
Queries -> async reads
Mutations -> async writes
React Binding -> render the model's ui
Testing -> layers, fakes, allSettled
Streams -> advanced pipelines
Lifetime -> scopes, TTL, finalizers