Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Testing
  2. Architecture rules
  3. Computed purity

Pure craftComputed derivations#

assertCraftComputedPure requires a craftComputed to read dependencies and return a value. It may not call a method or write to a source:

ts
export function keepDerivationsPure(graph: ArchitectureGraph) {
  assertCraftComputedPure(graph.graph);
}

The safe shape#

typescript
const remaining = craftUse(craftComputed('remaining', function* () {
  return (yield* tasks()).filter((task) => !task.done).length;
}));

What it prevents#

This looks convenient but makes a derivation an imperative workflow:

typescript
const count = craftUse(craftComputed('count', function* () {
  yield* audit.log('count recomputed');
  yield* tasks.set(normalizeTasks(yield* tasks()));
  return (yield* tasks()).length;
}));

Now reading count can write state, invoke a method, or trigger another computation. Re-computation order becomes observable, and a harmless template read can cause a loop.

The set can be hidden in a local function#

Moving the write into a helper does not make it a derivation. This is still invalid:

typescript
const tasks = yield* state(
  'tasks',
  initialTasks,
  ({ set }) => ({ set }),
);

const remaining = craftUse(craftComputed('remaining', function* () {
  // The write is not on the next line, but the helper belongs to this computed.
  const normalizeAndStore = function* (value: Task[]) {
    yield* tasks.set(normalizeTasks(value));
  };

  const current = yield* tasks();
  yield* normalizeAndStore(current);
  return current.filter((task) => !task.done).length;
}));

The graph still records the relationship:

craftComputed:remaining ──writes──▶ state:tasks

So assertCraftComputedPure rejects it even though the computed body only calls normalizeAndStore at the apparent call site. The failure points back to the computed and the write target, rather than relying on a reviewer to notice a set several lines down.

The same applies to an indirect method call:

typescript
const refresh = craftUse(craftMethod('refresh', function* () {
  yield* tasks.set(initialTasks);
}));

const count = craftUse(craftComputed('count', function* () {
  const runRefresh = () => refresh();
  yield* runRefresh();
  return (yield* tasks()).length;
}));

The rule sees the calls edge from count to refresh. This is why the check belongs on the graph in addition to a local ESLint rule: it protects the invariant even when the side effect is hidden behind a binding.

The fix is to keep the computed read-only and move the write to an explicit method or event:

typescript
const normalize = craftUse(craftMethod('normalize', function* () {
  yield* tasks.set(normalizeTasks(yield* tasks()));
}));

const remaining = craftUse(craftComputed('remaining', function* () {
  return (yield* tasks()).filter((task) => !task.done).length;
}));

Where the side effect belongs#

  • derive a value with craftComputed;

  • react to an event with on$;

  • update a primitive from a user action with a method;

  • run external work with craftEffect or an explicit resource primitive.

Separating these roles makes the dependency graph explainable and tests deterministic.

See also#

  • The mental model

  • craftComputed

←HTTP endpoint ownershipDependency cycles→

On this page

  • The safe shape
  • What it prevents
  • The set can be hidden in a local function
  • Where the side effect belongs
  • See also

@craft-ts · MIT licensed

GitHubnpm

Search