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. craftEffect and imperative sync

Keep craftEffect out of imperative synchronisation#

assertCraftEffectNoImperativeSync prevents a craftEffect from writing a state/source or triggering another query, mutation or async process:

ts
export function keepResourceTransitionsDeclarative(graph: ArchitectureGraph) {
  assertCraftEffectNoImperativeSync(graph.graph);
}

The syntax is valid — the placement is not#

The following calls are valid Craft generator syntax. set, call and mutate return yieldable operations, so a generator consumes them with yield*:

typescript
function* submit() {
  yield* searchResults.set(yield* rawResults());
  yield* usersQuery.call(yield* searchTerm());
  yield* saveMutation.mutate(yield* draft());
}

The problem is putting the same code in a craftEffect. This is exactly the case rejected by assertCraftEffectNoImperativeSync:

typescript
craftUse(craftEffect('sync', function* () {
  yield* searchResults.set(yield* rawResults());
  yield* usersQuery.call(yield* searchTerm());
  yield* saveMutation.mutate(yield* draft());
}));

The rule is therefore not saying that yield* searchResults.set(...) is invalid TypeScript or invalid Craft syntax. It is saying that a reactive effect must not imperatively write or trigger another Craft primitive.

What it prevents#

This effect creates three hidden edges in the Craft graph:

typescript
craftUse(craftEffect('sync', function* () {
  yield* searchResults.set(yield* rawResults());
  yield* usersQuery.call(yield* searchTerm());
  yield* saveMutation.mutate(yield* draft());
}));

The graph is effectively:

sync effect ──writes──▶ searchResults
           ├─calls────▶ usersQuery
           └─calls────▶ saveMutation

Whenever one of the values read by the effect changes, the effect can write state, start a query and start a mutation again. The direction of data flow is hidden in a callback, which can create feedback loops, duplicate requests or a mutation that runs merely because a signal was read.

Use the primitive that owns the relationship instead#

If the query depends on searchTerm, make that dependency explicit with params:

typescript
const usersQuery =
  yield *
  query('usersQuery', {
    params: searchTerm,
    loader: ({ params }) => searchUsers(params),
  });

If the operation is a single explicit user action, a craftMethod may call one mutation after normalising the event:

typescript
const save = craftUse(craftMethod('save', function* (event: Event) {
  event.preventDefault();
  yield* saveMutation.mutate(yield* draft());
}));

For several operations belonging to one event, emit a source$ directly from the submit or click handler and let each affected primitive react to it. A mutation-to-query relationship belongs in insertReactOnMutation, not beside the mutation call site:

typescript
const signOut$ = source$<void>('signOut$');

button({ click: () => signOut$.emit() }, 'Sign out');

const logout =
  yield *
  mutation('logout', {
    method: signOut$.asReadonly(),
    loader: logoutUser,
  });

const session =
  yield *
  query(
    'session',
    { params: () => 'current', loader: loadSession },
    insertReactOnMutation(logout, {
      optimisticUpdate: () => undefined,
    }),
  );

craft-ts/no-imperative-craft-method-actions and craft-ts/no-imperative-storage-in-craft-method enforce this placement in the editor. Storage adapters and intentionally imperative facades remain valid in their craftService seam.

For a mutation-to-query relationship, use an insertion such as insertReactOnMutation. For a named external event, use on$. Use a computed value when searchResults is only a transformation of rawResults, instead of storing a second value and synchronising it.

Logging, focus and other effects that do not push into Craft primitives remain valid. The rule protects synchronization, not all side effects.

See also#

  • Reacting to mutations

  • From event to source

←craftEffect and networkInteractive element names→

On this page

  • The syntax is valid — the placement is not
  • What it prevents
  • Use the primitive that owns the relationship instead
  • See also

@craft-ts · MIT licensed

GitHubnpm

Search