State
Pitfalls And API
Review common mistakes, exact API shapes, and recommended defaults for new state code.
- Pitfalls
- API reference
- Defaults
- Review checklist
Use this page while reviewing code or checking exact API shapes.
Common Pitfalls
Forgetting Token Cleanup
Bad:
private void OnEnable()
{
Health.Subscribe(OnHealthChanged);
}Good:
private void OnEnable()
{
Health.Subscribe(OnHealthChanged).AddTo(_bindings);
}
private void OnDisable()
{
_bindings.DisposeAll();
}Subscribing Off-Thread
Bad:
Task.Run(() => Health.Subscribe(OnHealthChanged));Good:
private void OnEnable()
{
Health.Subscribe(OnHealthChanged).AddTo(_bindings);
}Off-thread SourceSet is supported. Off-thread Subscribe is not the intended usage.
Expecting Scheduled Notifications Immediately
var score = new AzState<int>(0, schedule: AzStateSchedule.NextUpdate);
score.SourceSet(100);
Debug.Log(score.Value); // main-thread SourceSet updates value now
// Subscribers are notified later.Use Immediate if subscriber callbacks must run synchronously.
In-Place Mutation
Always write through SourceSet.
var position = Position.Value;
position.x = 10;
// This modified only the local copy for structs.
// For reference types, mutating in place may bypass equality and notification.Good:
var position = Position.Value;
Position.SourceSet(new Vector3(10, position.y, position.z));Reference Type Equality
Data.SourceSet(new MyData { Value = 10 });
Data.SourceSet(new MyData { Value = 10 }); // may notify again by reference inequalityProvide a comparer if content equality matters.
Weak Lambdas
Avoid weak subscriptions with anonymous lambdas that you do not store.
Health.Subscribe(value => UpdateUi(value), weak: true);Prefer:
Health.Subscribe(OnHealthChanged, weak: true);Paused SubscribeChanged Jumps
SubscribeChanged keeps its own previous value. If the token is paused, skipped notifications do not update that previous value.
After resume, the next callback may report a larger old-to-new jump than expected.
Two-Way Binding Without Reconciliation
Pause lets both sides diverge. Resume does not automatically choose a winner.
binding.Resume();
UiVolume.SourceSet(ModelVolume.Value, force: true); // explicit reconciliationAPI Quick Reference
AzStateSchedule
public enum AzStateSchedule : byte
{
Immediate = 0,
EndOfFrame = 1,
NextUpdate = 2
}AzStateValidationResult
public readonly struct AzStateValidationResult
{
public readonly bool IsValid;
public readonly string? ErrorMessage;
public static AzStateValidationResult Success();
public static AzStateValidationResult Failure(string error);
}AzStateBatch
public readonly struct AzStateBatch : IDisposable
{
public static AzStateBatch Begin();
public void Dispose();
}AzState<T>
public sealed class AzState<T>
{
public Func<T, T>? Validator { get; set; }
public Func<T, AzStateValidationResult>? StrictValidator { get; set; }
public AzState(T initial, IEqualityComparer<T?>? comparer = null, AzStateSchedule schedule = AzStateSchedule.Immediate);
public AzState(IEqualityComparer<T?>? comparer = null, AzStateSchedule schedule = AzStateSchedule.Immediate);
public T Value { get; }
public bool HasValue { get; }
public int Version { get; }
public AzStateSchedule Schedule { get; }
public T ValueSource { get; set; }
public bool SourceSet(T v, bool force = false, bool validateOnlyIfChanged = true);
public bool TrySourceSet(T v, out AzStateValidationResult result, bool force = false);
public void Refresh();
public AzEventToken Subscribe(Action<T> sink, bool notifyImmediately = true, int priority = 0, bool weak = false);
public void Subscribe(Action<T> sink, out AzEventToken token, bool notifyImmediately = true, int priority = 0, bool weak = false);
public AzEventToken SubscribeMap<TU>(Func<T, TU> map, Action<TU> sink, bool notifyImmediately = true, int priority = 0, bool weak = false);
public void SubscribeMap<TU>(Func<T, TU> map, Action<TU> sink, out AzEventToken token, bool notifyImmediately = true, int priority = 0, bool weak = false);
public AzEventToken SubscribeChanged(Action<T, T> onChanged, int priority = 0);
public void SubscribeChanged(Action<T, T> onChanged, out AzEventToken token, int priority = 0);
public AzEventToken BindTo(Action<T> setter, bool notifyImmediately = true, int priority = 0, bool weak = false);
public AzEventToken BindTo<TObj>(TObj obj, Action<TObj, T> setter, bool notifyImmediately = true, int priority = 0) where TObj : class;
public IAzPausableToken BindTwoWay(AzState<T> other, bool snapToThisOnBind = false);
public void BindTwoWay(AzState<T> other, out IAzPausableToken token, bool snapToThisOnBind = false);
public void SetSchedule(AzStateSchedule newSchedule);
public void UnsubscribeAll();
public void ResetToDefault();
}IReadOnlyAzState<T>
public interface IReadOnlyAzState<T>
{
T Value { get; }
int Version { get; }
bool HasValue { get; }
AzStateSchedule Schedule { get; }
AzEventToken Subscribe(Action<T> sink, bool notifyImmediately = true, int priority = 0, bool weak = false);
AzEventToken SubscribeMap<U>(Func<T, U> map, Action<U> sink, bool notifyImmediately = true, int priority = 0, bool weak = false);
AzEventToken SubscribeChanged(Action<T, T> onChanged, int priority = 0);
AzEventToken BindTo(Action<T> setter, bool notifyImmediately = true, int priority = 0, bool weak = false);
AzEventToken BindTo<TObj>(TObj obj, Action<TObj, T> setter, bool notifyImmediately = true, int priority = 0) where TObj : class;
}
public static IReadOnlyAzState<T> AsReadOnly<T>(this AzState<T> source);AzDomainProperty<T>
public sealed class AzDomainProperty<T>
{
public AzDomainProperty(
string propertyName,
T initialValue,
Action<string, T?, T>? onDomainChange = null,
IEqualityComparer<T?>? comparer = null,
AzStateSchedule schedule = AzStateSchedule.Immediate);
public T Value { get; }
public int Version { get; }
public bool HasValue { get; }
public string PropertyName { get; }
public Func<T, T>? Validator { get; set; }
public Func<T, AzStateValidationResult>? StrictValidator { get; set; }
public bool Set(T value, bool force = false);
public bool TrySet(T value, out AzStateValidationResult result, bool force = false);
public AzEventToken Subscribe(Action<T> sink, bool notifyImmediately = true, int priority = 0, bool weak = false);
public IReadOnlyAzState<T> AsReadOnly();
public AzEventToken BindTo<TObj>(TObj obj, Action<TObj, T> setter, bool notifyImmediately = true, int priority = 0) where TObj : class;
public void Refresh();
}AzPipelinedState<T>
public sealed class AzPipelinedState<T>
{
public AzPipelinedState(T initialValue, IEqualityComparer<T?>? comparer = null, AzStateSchedule schedule = AzStateSchedule.Immediate);
public AzPipelinedState(IEqualityComparer<T?>? comparer = null, AzStateSchedule schedule = AzStateSchedule.Immediate);
public T Value { get; }
public int Version { get; }
public bool HasValue { get; }
public void Use(Func<AzStateMiddlewareContext<T>, bool> middleware);
public bool SourceSet(T value, bool force = false);
public AzEventToken Subscribe(Action<T> sink, bool notifyImmediately = true, int priority = 0, bool weak = false);
public IReadOnlyAzState<T> AsReadOnly();
}Recommended Defaults for New Users
Use these defaults until you need something more specialized:
- Use
AzState<T>for values that have a current state. - Use
AzEventorAzPriorityEventfor discrete signals. - Give state an initial value when possible.
- Mutate state with
SourceSet. - Subscribe on the main thread.
- Put returned tokens in an
AzEventTokenBag. - Call
DisposeAll()inOnDisable. - Use
BindTofor UI setters. - Use
SubscribeMapfor formatting and type conversion. - Use
SubscribeChangedonly when old/new values matter. - Use
Immediateby default. - Use
NextUpdatefor coalesced UI-heavy values. - Use
EndOfFramefor late presentation values. - Use
Validatorto transform values. - Use
StrictValidatorplusTrySourceSetfor user-facing validation. - Store
BindTwoWaytokens separately asIAzPausableToken. - Expose
IReadOnlyAzState<T>from public models.
