Core Concepts
Readonly properties
Expose a readonly property while still keeping it reactive through explicit state updates.
Practical value
Key points
- Use a private state id (for example `_aPlusBId = "aPlusB"`) to store readonly computed state.
- Expose the readonly property via a getter that reads from `stateManager.getState(this, id)`.
- Recompute and write the value internally with `stateManager.setState(...)` when source fields change.
- Subscribe to `stateManager.changed` if you need to observe emitted updates.
- Always call `releaseState(...)` when the model instance is no longer needed.
How the pattern works
In the example, `aPlusB` is readonly for callers because it only has a getter. Internally, the model stores that value in StateManager using a private key (`_aPlusBId`).
Whenever `a` or `b` changes, `setAPlusB()` recomputes the new value and writes it through `stateManager.setState(this, _aPlusBId, ...)`.
Why this is still reactive
The readonly getter returns `stateManager.getState(this, _aPlusBId)`, so reads always use the latest committed value.
Because updates go through StateManager, `changed` emissions are produced for the readonly value key, so subscribers can react just like with normal writable state.
Ownership and cleanup
This pattern makes ownership explicit: only the model is allowed to mutate the readonly result, through one internal method.
When the model is disposed, release the registered state id. This prevents stale watches and keeps memory/subscriptions clean.
When to use this approach
Use it when a value should be public and reactive, but never directly assignable by consumers (for example totals, derived status flags, validation summaries, or normalized snapshots).
It is a good fit for domain models where you want strict write control and predictable change propagation.
Example
import { InjectionContainer, printValue } from '@rs-x/core';
import {
rsx,
RsXExpressionParserModule,
type IExpression,
} from '@rs-x/expression-parser';
import {
type IStateChange,
type IStateManager,
RsXStateManagerInjectionTokens,
} from '@rs-x/state-manager';
await InjectionContainer.load(RsXExpressionParserModule);
const stateManager: IStateManager = InjectionContainer.get(
RsXStateManagerInjectionTokens.IStateManager,
);
class MyModel {
private readonly _aPlusBId = 'aPlusB';
private _a = 10;
private _b = 20;