The States
Defining a Stateβ
A state represents a slice of your application's data. It should be:
- Immutable (defined as a
record
) - Serializable (for debugging, testing, or persistence)
- Focused (represents a single domain or feature)
Example: Counter Stateβ
public record CounterState : IStateFeature
{
public int Count { get; init; }
}
π― Manual State Hookingβ
The manual subscription approach is the most performant and reliable way to handle state updates β in StatePulse or any other state management library.
By explicitly subscribing and unsubscribing to a specific state, only the components that depend on that state are re-rendered.
This avoids unnecessary rendering and offers precise control.
β Why This Is Optimalβ
- No overhead from global tracking or base components
- Fine-grained control over component updates
- Best performance, especially in large apps
- Works without any framework-specific magic
β οΈ The downside? It requires more boilerplate.
That's why many libraries introduce alternatives like global components, wrappers, or base components β but these come with trade-offs in flexibility or overhead.
π οΈ Example: Manual State Hookβ
public partial class Counter : ComponentBase, IAsyncDisposable
{
[Inject] IStateAccessor<CounterState> State { get; set; }
[Inject] private IDispatcher Dispatcher { get; set; }
protected override void OnInitialized()
{
State.OnStateChangedNoDetails += ShouldUpdate;
}
private void ShouldUpdate(object? sender, EventArgs e)
{
_ = InvokeAsync(StateHasChanged);
}
private async Task Increment()
{
await Dispatcher.Prepare<IncrementCounterAction>()
.With(p => p.Delay, 1)
.DispatchAsync();
}
public ValueTask DisposeAsync()
{
State.OnStateChangedNoDetails -= ShouldUpdate;
return ValueTask.CompletedTask;
}
}
π Zero-Boilerplate Without Compromiseβ
StatePulse does not force you to inherit from a base component or use a global component to track state changes.
Instead, StatePulse provides a clever and efficient mechanism to track components that request state,
binding them automatically with:
- β Memory leak protection
- β‘ Optimized getters
- π§Ό Zero boilerplate on your side
This ensures no architectural compromises burden placed on you.
StatePulse offers a zero-boilerplate way to subscribe to and track component-bound state changes β using IStatePulse
.
This method requires no manual subscription or disposal, yet still tracks updates per component, safely and efficiently.
π οΈ Example: Zero-Boilerplate State Hookβ
public partial class Counter : ComponentBase
{
[Inject] IStatePulse Pulse { get; set; }
private CounterState State => Pulse.StateOf<CounterState>(() => this, ShouldUpdate);
[Inject] private IDispatcher Dispatcher { get; set; }
public Task ShouldUpdate() => InvokeAsync(StateHasChanged);
private async Task Increment()
{
await Dispatcher.Prepare<IncrementCounterAction>()
.With(p => p.Delay, 1)
.DispatchAsync();
}
}
π Note on
StateOf()
Usage
You might notice that Pulse.StateOf<CounterState>(() => this, ShouldUpdate)
is called during every render.
At first glance, this may seem inefficient β but itβs actually intentional and necessary.
This method guarantees that the component is correctly bound to the state and always get latest state.
Without using this shorthand, youβd be forced to call StateOf(...).Property
directly in your Razor markup, which becomes less readable and harder to maintain.
When StateOf()
is called:
- StatePulse checks whether the component (identified by
()=> this
) is already being tracked. - If not, it sets up the binding and associates the provided
ShouldUpdate
method as a re-render callback. - If it is already tracked, the call becomes a fast property access with near-zero overhead.
β‘ The only cost is during the initial setup.
All future calls are optimized and safe to run on every render.
This design ensures you always have up-to-date, reactive state with no boilerplate and minimal performance impact.