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. Typed insertion pipes

Typed insertion pipes#

Each primitive accepts one insertion directly. When a primitive needs several insertions, use the pipe named after that primitive:

PrimitiveTyped pipe
stateinsertStatePipe
queryinsertQueryPipe
mutationinsertMutationPipe
queryParamsinsertQueryParamsPipe
asyncProcessinsertAsyncProcessPipe
craftStateMachineinsertStateMachinePipe

The typed pipe keeps the primitive call readable and gives every member the correct contextual type. Members run from left to right, and each member can read the outputs of the members before it through insertions.

State#

typescript
import { craftComputed, insertStatePipe, state, craftUse } from '@craft-ts/core';

const { counter } =
  yield *
  state(
    'counter',
    0,
    insertStatePipe(
      ({ update }) => ({
        increment: () => update((value) => value + 1),
      }),
      ({ state, insertions }) => ({
        isOdd: craftUse(craftComputed('isOdd', function* () {
          return (yield* state()) % 2 === 1;
        })),
        incrementAndReport: function* () {
          yield* insertions.increment();
          return yield* state();
        },
      }),
    ),
  );

Query#

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

const { users } =
  yield *
  query(
    'users',
    {
      params: () => ({ page: 1 }),
      loader: ({ params }) => api.getUsers(params),
    },
    insertQueryPipe(
      insertStoragePersister(craftUnique({
        storeName: 'app',
        key: 'users',
      })),
      ({ resource }) => ({
        reloadUsers: function* () {
          return yield* resource.reload();
        },
      }),
    ),
  );

Mutation#

typescript
import { insertMutationPipe, mutation } from '@craft-ts/core';

const { saveUser } =
  yield *
  mutation(
    'saveUser',
    {
      method: (user: User) => user,
      loader: ({ params }) => api.saveUser(params),
    },
  insertMutationPipe(
    ({ resource }) => ({
      reload: function* () {
        return yield* resource.reload();
      },
    }),
    ({ insertions }) => ({
      reloadTwice: function* () {
        yield* insertions.reload();
        yield* insertions.reload();
      },
      }),
    ),
  );

URL state#

typescript
import { craftComputed, insertQueryParamsPipe, queryParams, craftUse } from '@craft-ts/core';

const { filters } =
  yield *
  queryParams(
    'filters',
    {
      state: {
        page: { fallbackValue: 1 },
        search: { fallbackValue: '' },
      },
    },
    insertQueryParamsPipe(
      ({ state }) => ({
        hasSearch: craftUse(craftComputed('hasSearch', function* () {
          return (yield* state()).search.length > 0;
        })),
      }),
      ({ state, patch }) => ({
        nextPage: function* () {
          const current = yield* state();
          return yield* patch({ page: current.page + 1 });
        },
      }),
    ),
  );

Async process#

typescript
import { insertAsyncProcessPipe, asyncProcess } from '@craft-ts/core';

const { search } =
  yield *
  asyncProcess(
    'search',
    {
      method: (term: string) => term,
      loader: ({ params }) => api.search(params),
    },
  insertAsyncProcessPipe(
    () => ({ source: 'search-box' as const }),
    ({ insertions }) => ({
      prefixTerm: (term: string) => `${insertions.source}:${term}`,
    }),
    ),
  );

When to use craftPipe#

Use a single insertion directly when there is no composition:

typescript
state('counter', 0, ({ update }) => ({
  increment: () => update((value) => value + 1),
}));

Keep craftPipe for universal compositions that need an explicit context, especially nested insertions such as insertSelect:

typescript
state('board', initialBoard, (context) =>
  craftPipe(
    context,
    insertSelect('grid', (gridContext) =>
      craftPipe(gridContext, ({ update }) => ({
        reset: () => update(() => []),
      })),
    ),
    ({ state }) => ({
      rowCount: craftUse(craftComputed('rowCount', function* () {
        return (yield* state()).grid.length;
      })),
    }),
  ),
);

The typed pipes delegate to craftPipe, so their runtime semantics remain the same: insertion outputs are merged left to right, generator insertions are driven, and each member keeps its own observability wrapper.

←InsertionsExceptions as values→

On this page

  • State
  • Query
  • Mutation
  • URL state
  • Async process
  • When to use craftPipe

@craft-ts · MIT licensed

GitHubnpm

Search