state
The state primitive creates a Signal-based state with optional insertions for adding methods and computed properties.
Import
import { state } from '@craft-ng/core';Basic Examples
Simple state with a primitive value
const counter = state(0);
console.log(counter()); // 0State with a linkedSignal
import { signal, linkedSignal } from '@angular/core';
const origin = signal(5);
const doubled = state(linkedSignal(() => origin() * 2));
console.log(doubled()); // 10State with insertions to add methods
import { signal, linkedSignal, computed } from '@angular/core';
const origin = signal(5);
const counter = state(
linkedSignal(() => origin() * 2),
({ update, set }) => ({
increment: () => update((current) => current + 1),
reset: () => set(0),
}),
);
console.log(counter()); // 10
counter.increment();
console.log(counter()); // 11
counter.reset();
console.log(counter()); // 0State with multiple insertions (methods and computed properties)
import { signal, linkedSignal, computed } from '@angular/core';
const origin = signal(5);
const counter = state(
linkedSignal(() => origin() * 2),
({ update, set }) => ({
increment: () => update((current) => current + 1),
reset: () => set(0),
}),
({ state }) => ({
isOdd: computed(() => state() % 2 === 1),
}),
);
console.log(counter()); // 10
console.log(counter.isOdd()); // false
counter.increment();
console.log(counter()); // 11
console.log(counter.isOdd()); // trueState with source binding
Methods bound to sources using on$ are not exposed on the state, they only work internally:
const sourceSignal = source$<number>();
const myState = state(0, ({ set }) => ({
setValue: on$(sourceSignal, (value) => set(value)),
reset: () => set(0),
}));
console.log(myState()); // 0
// Note: setValue is not exposed on myState, only used internally
sourceSignal.emit(34);
console.log(myState()); // 34
myState.reset();
console.log(myState()); // 0Parallel States
You can generate multiple state instances in parallel with a single state(...) declaration.
1) Parallel state with method + state
const todosById = state(
{
method: (id: number) => id,
state: ({ params: id }) => ({ id, done: false }),
},
({ stateById }) => ({
toggle: (id: number) =>
stateById.select(id)?.update((todo) => ({ ...todo, done: !todo.done })),
}),
);
todosById.create(1);
console.log(todosById.select(1)); // { id: 1, done: false }
todosById.toggle(1);
console.log(todosById.select(1)); // { id: 1, done: true }2) Parallel state from params signal
const currentId = signal(0);
const itemById = state({
params: currentId,
identifier: (id) => id,
state: ({ params: id }) => ({ id, selected: false }),
});
console.log(itemById(0)); // { id: 0, selected: false }
currentId.set(1);
console.log(itemById(1)); // { id: 1, selected: false }3) Parallel state from from signal (array/object)
const ids = signal([0, 1, 2]);
const rows = state({
from: ids,
identifier: ({ index }) => index,
state: ({ params: { index } }) => ({ index, active: false }),
});
console.log(rows(0)); // { index: 0, active: false }Typing Note
For better TypeScript inference, pass Angular Signal values (signal, linkedSignal) rather than manually widening to WritableSignal.
This avoids overload inference limitations in complex generic calls.
Best Practices
✅ Use TypeScript inference - Let TypeScript infer types when possible ✅ Keep state granular - Create focused state slices ✅ Use computed for derived state - Don't duplicate state ✅ Use insertions - Add custom methods and computed properties to extend functionality
See Also
- AsyncProcess - For async operations
- Store - For composing multiple states
- Insertions - For enhancing state behavior