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);
public Task ShouldUpdate() => InvokeAsync(StateHasChanged);
private async Task Increment()
{
await Pulse.Dispatcher.Prepare<IncrementCounterAction>()
.With(p => p.Delay, 1)
.DispatchAsync();
}
}
Note: when injecting IStatePulse, you do not need to inject IDispatcher.
π 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 or even odd syntax but itβs actually intentional, efficient and necessary.
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
ShouldUpdatemethod as a re-render callback (Slow first hit). - If it is already tracked, the call becomes a fast property access with near-zero overhead (subsequent accesses are fast).
Mandatory Syntax
As of v2.0+ compiler will generate an error when ()=> this is not exactly that and when ShouldUpdate is a lambda which is forbidden and has to be named method to avoid runtime issues.
The only cost is during the initial setup.
All future calls are optimized, cached and safe to run on every render, on every line.
This design ensures you always have up-to-date, reactive state with no boilerplate and minimal performance impact.