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. Core concepts
  2. Insertions

Insertions#

An insertion is a function that receives a primitive's internals and returns what to expose on it. It is how behaviour gets attached to state — and how it gets reused.

Use one whenever a primitive needs methods, computed values, or a ready-made behaviour like storage persistence. Every primitive accepts one insertion directly. For several insertions, prefer the typed helper for that primitive; see Typed insertion pipes. Use craftPipe when you need a universal pipe or an explicit nested context.

The common case#

The library's insertions and the ones you write are the same shape, so they compose in the same pipe:

typescript
import {
  craftUnique,
  insertStoragePersister,
  insertPaginationPlaceholderData,
  insertReactOnMutation,
  insertQueryPipe,
  insertStatePipe,
  query,
} from '@craft-ts/core';

const users = yield* query(
  'users',
  {
    params: pagination,
    identifier: (params) => `${params.page}-${params.pageSize}`,
    loader: function* ({ params }) {
      return yield* ApiService.getDataList(params);
    },
  },
  insertQueryPipe(
    insertStoragePersister(craftUnique({
      storeName: 'app',
      key: 'users',
    })),
    insertPaginationPlaceholderData({ initialValue: [] as User[] }),
    insertReactOnMutation(deleteUser, {
      filter: ({ mutationIdentifier, queryResource }) =>
        !!queryResource.value()?.some((u) => u.id === mutationIdentifier),
      optimisticUpdate: ({ queryResource, mutationIdentifier }) =>
        removeOne({
          entities: queryResource.value(),
          id: mutationIdentifier,
        }),
    }),
  ),
);

The typed helper supplies the query context to each member and keeps the primitive call free of context plumbing.

Deep-yieldable collection items#

When a component reads several properties from the same forNode item, expose an explicit deep-yieldable view of the collection. The original collection keeps its existing contract, while the named view gives the item callback lazy readers for the item's properties:

Before:

typescript
forNode(catalog.products, { track: (product) => product.id }, (product) =>
  article([
    span(function* () {
      return (yield* product()).category;
    }),
    span(function* () {
      return (yield* product()).name;
    }),
  ]),
);

After:

typescript
import { insertDeepYieldable, state } from '@craft-ts/core';

const catalog = yield* state(
  'catalog',
  { products },
  insertDeepYieldable('products'),
);

forNode(
  catalog.deepYieldableProducts,
  { track: (product) => product.id },
  (product) => article([span(product.category), span(product.name)]),
);

insertDeepYieldable('products') leaves catalog.products unchanged and adds catalog.deepYieldableProducts. forNode applies the item projection only to the named deep reader; ordinary lists keep their existing yield* item() contract. The craft-ts/prefer-deep-yieldable-for-item ESLint rule warns when the before pattern is repeated in a component template.

Deep projections of query values#

When a query returns an object, use insertDeepYieldableValue() when its properties are consumed by the template. The insertion targets value only, so the primitive keeps its normal API while the resolved object exposes lazy, yieldable property readers:

typescript
import {
  insertDeepYieldableValue,
  query,
} from '@craft-ts/core';

const productQuery = yield* query(
  'productDetails',
  {
    method: (id: string) => id,
    loader: ({ params }) => api.getProduct(params),
  },
  insertDeepYieldableValue(),
);

// In a template: productQuery.value.name
// In a generator: yield* productQuery.value.name()

For an identified query, the same insertion is applied to the selected resource values:

typescript
const product = productQuery.select(productId);
if (product) {
  yield* product.value.name();
}

This is deliberately different from insertDeepYieldable(), which adapts the primitive's root value and is still useful for object-valued state.

A single insertion needs no pipe

Pass it directly:

typescript
const user = yield* query('user', config, insertStoragePersister({ … }));

Writing your own#

There is nothing special about a library insertion. Yours is a function of the same shape:

typescript
const counter = yield* state(
  'counter',
  0,
  insertStatePipe(
    ({ update, set }) => ({
      increment: () => update((c) => c + 1),
      reset: () => set(0),
    }),
    ({ state }) => ({
      isOdd: craftUse(craftComputed('isOdd', function* () {
        return (yield* state()) % 2 === 1;
      })),
    }),
  ),
);

Extract it to a named function the moment two primitives want the same behaviour — that is the whole extension mechanism.

A member can also be a function*, in which case it can yield* services and those dependencies fold into the enclosing graph. A craftComputed or generator method must yield every reader it does not own — including this primitive's state() / update() / sibling methods on insertions.

What piping guarantees#

Piping is strictly equivalent to attaching members one by one:

  • members run left to right;

  • each member sees the previous members' outputs on context.insertions;

  • the outputs are the intersection of all members' — on a key conflict, the rightmost wins at runtime;

  • tracked dependencies are the union of all members', so ExtractDeps sees every one;

  • each member is wrapped individually, so correlation-id tracking and app snapshots observe them separately.

Nesting#

Pipes nest freely, including inside insertSelect — each level re-passes its own context:

typescript
const board = yield* state(
  'board',
  { ui: { activeColor: 'black' }, grid: createInitialGrid() },
  insertStatePipe(
    insertStoragePersister(craftUnique({
      storeName: 'app',
      key: 'board',
    })),
    () => ({ resetAll$: source$<void>('resetAll$') }),
    insertSelect('grid', (gridContext) =>
      craftPipe(
        gridContext,
        ({ state, update }) => ({
          addRow: () => update((grid) => [...grid, createNextRow(grid)]),
          rowIndexes: craftUse(craftComputed('rowIndexes', function* () {
            return (yield* state()).map((_row, index) => index);
          })),
        }),
        insertSelect('row', ({ update }) => ({
          /* … */
        })),
      ),
    ),
  ),
);

Pitfalls#

Choosing the wrong pipe. Use the primitive-specific helper for a direct composition. craftPipe still requires an explicit context and is the right choice for universal or nested compositions.

Two members exporting the same key. The rightmost wins silently at runtime. Name your outputs so they don't collide.

Why the context is explicit

It is what makes one universal pipe possible for all five primitives. The outer (context) => … lambda is contextually typed by the primitive, so TypeScript knows the exact context shape before it resolves the craftPipe call. Inline lambdas keep full contextual typing, higher-order factories like insertReactOnMutation(...) match as before, and the primitive's Exceptions inference is never degraded.

See Also#

  • Anatomy of a primitive

  • Injectable runtime context — recovering set / update / patch from DI, including for WebMCP

  • Selecting — insertSelect and nested insertions

  • Reacting to mutations

←Generators and yield*Typed insertion pipes→

On this page

  • The common case
  • Deep-yieldable collection items
  • Deep projections of query values
  • Writing your own
  • What piping guarantees
  • Nesting
  • Pitfalls
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search