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. Managing state
  2. Selecting

Selecting a sub-state#

insertSelect targets a nested part of a state and attaches insertions to that part, so the logic lives next to the data it operates on rather than at the top of a deeply nested object.

Use it when a state is a tree and a method only concerns one branch: a cell in a grid, a row in a table, one section of a settings object. Not when the whole state is the subject — a plain insertion is simpler.

One API covers both shapes: the parent can be an object or an array, and you don't switch helpers based on which.

`state` only

This insertion works with the state primitive.

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

The common case — selecting an object property#

typescript
const board = yield* state(
  'board',
  {
    cell: {
      color: 'white',
      paintCount: 0,
    },
  },
  insertSelect('cell', ({ update, state }) => ({
    paint: () =>
      update((cell) => ({
        ...cell,
        color: 'black',
        paintCount: cell.paintCount + 1,
      })),
    paintCountStr: function* () {
      return `Painted ${(yield* state()).paintCount} times`;
    },
  })),
);

yield* board.selectCell().paint();
yield* board.selectCell().paintCountStr(); // "Painted 1 times"

Selecting into an array#

typescript
const cells = yield* state(
  'cells',
  [{ color: 'white', paintCount: 0 }],
  insertSelect('cell', ({ update }) => ({
    paint: () =>
      update((cell) => ({
        ...cell,
        color: 'black',
        paintCount: cell.paintCount + 1,
      })),
  })),
);

const cell = cells.selectCell(0);
if (cell) yield* cell.paint();
console.log(cells.selectCell(0)?.paintCount); // 1

Yielding dependencies#

typescript
insertSelect('cell', function* ({ patch }) {
  const color = yield* ColorService();
  return {
    paint: () =>
      patch(() => ({
        color,
      })),
  };
});

The dependencies are tracked at the primitive level.

Pitfalls#

Only object properties can be selected. On an object state, targeting a property that is not itself an object — a string, number or boolean — is not supported yet, and currently breaks type inference rather than failing cleanly. An improvement is planned.

A select takes a single nested insertion, like any primitive. Use craftPipe for more than one nested insertion (below).

Nested typing needs no anchor

Use craftPipe when composing nested insertSelect levels because each level has its own explicit context. The historical insertNoopTypingAnchor workaround is not needed here — it remains necessary for the form-tree helpers.

Attaching several insertions#

Like the primitives, insertSelect accepts a single nested insertion. To attach several, re-pass the selected context through craftPipe:

ts
state(
  'board',
  { grid: createInitialGrid() },
  insertSelect('grid', (gridContext) =>
    craftPipe(
      gridContext,
      ({ state, update }) => ({
        addRow: () => update((grid) => [...grid, createNextRow(grid)]),
      }),
      insertSelect('row', ({ update }) => ({
        // ...
      })),
    ),
  ),
);

insertSelect also composes as a member of a pipe:

ts
state('cells', initialCells, insertStatePipe(
    insertStoragePersister(craftUnique({
      storeName: 'app',
      key: 'cells',
    })),
    insertSelect('cell', ({ update }) => ({
      paint: () => update((cell) => ({ ...cell, painted: true })),
    })),
  ));
Working examples — pixel art

Two demos built almost entirely on nested selects:

  • Pixel Art (1D grid)

  • Pixel Art Matrix (2D grid)

See Also#

  • Insertions — composing several on one primitive

  • Local state

  • Collections — for entity lists specifically

  • Architecture rules — assertInsertSelectUnique when two selects share a key on one host

←asyncProcessReacting to mutations→

On this page

  • The common case — selecting an object property
  • Selecting into an array
  • Yielding dependencies
  • Pitfalls
  • Attaching several insertions
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search