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 inequality

Provide 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 reconciliation

API 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 AzEvent or AzPriorityEvent for 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() in OnDisable.
  • Use BindTo for UI setters.
  • Use SubscribeMap for formatting and type conversion.
  • Use SubscribeChanged only when old/new values matter.
  • Use Immediate by default.
  • Use NextUpdate for coalesced UI-heavy values.
  • Use EndOfFrame for late presentation values.
  • Use Validator to transform values.
  • Use StrictValidator plus TrySourceSet for user-facing validation.
  • Store BindTwoWay tokens separately as IAzPausableToken.
  • Expose IReadOnlyAzState<T> from public models.