State

Unlike a feed an IState<T>, as its name suggests, is state-full. While a feed is just a query of a stream of data, a state also implies a current value (a.k.a. the state of the application) that can be accessed and updated.

There are some noticeable differences with a feed:

  • When subscribing to a state, the currently loaded value is going to be replayed.
  • There is a Update method that allows you to change the current value.
  • States are attached to an owner and share the same lifetime as that owner.
  • The main usage of state is for two-way bindings.

Sources: How to maintain a data

You can create a state using one of the following:

Empty

Creates a state without any initial value.

public IState<string> City => State<string>.Empty(this);

Value

Creates a state with a synchronous initial value.

public IState<string> City => State.Value(this, () => "Montréal");

Async

Creates a state with an asynchronous initial value.

public IState<string> City => State.Async(this, async ct => await _locationService.GetCurrentCity(ct));

AsyncEnumerable

Like for Feed.AsyncEnumerable, this allows you to adapt an IAsyncEnumerable<T> into a state.

public IState<string> City => State.AsyncEnumerable(this, () => GetCurrentCity());

public async IAsyncEnumerable<string> GetCurrentCity([EnumeratorCancellation] CancellationToken ct = default)
{
	while (!ct.IsCancellationRequested)
	{
		yield return await _locationService.GetCurrentCity(ct);
		await Task.Delay(TimeSpan.FromMinutes(15), ct);
	}
}

Create

This gives you the ability to create your own state by dealing directly with messages.

This is designed for advanced usage and should probably not be used directly in apps.

public IState<string> City => State.Create(this, GetCurrentCity);

public async IAsyncEnumerable<Message<string>> GetCurrentCity([EnumeratorCancellation] CancellationToken ct = default)
{
	var message = Message<string>.Initial;
	var city = Option<string>.Undefined();
	var error = default(Exception);
	while (!ct.IsCancellationRequested)
	{
		try
		{
			city = await _locationService.GetCurrentCity(ct);
			error = default;
		}
		catch (Exception ex)
		{
			error = ex;
		}

		yield return message = message.With().Data(city).Error(error);
		await Task.Delay(TimeSpan.FromHours(1), ct);
	}
}

Update: How to update a state

The state is designed to allow to respect the ACID properties. This means that all update methods are requesting a delegate that accepts the current value to update. This makes sure that you are working with the latest version of the data.

Important

The provided delegate might be invoked more than once in case of concurrent updates. It must be a pure function (i.e. it must not alter anything else than the provided data).

UpdateValue

This allows you to update the value only of the state.

public IState<string> City => State<string>.Empty(this);

public async ValueTask SetCurrent(CancellationToken ct)
{
	var city = await _locationService.GetCurrentCity(ct);
	await City.UpdateValue(_ => city, ct);
}

Set

For value types and strings, you also have a Set which does not ensure the respect of the ACID properties.

public IState<string> Error => State<string>.Empty(this);

public async ValueTask Share(CancellationToken ct)
{
	try
	{
		../..
		await Error.Set(string.Empty, ct);
	}
	catch (Exception error)
	{
		await Error.Set("Share failed.", ct);
	}
}
Caution

This is designed for simple properties that are independent of their previous. You must not use a previously captured value of the state like this as it would break the ACID properties:

public IState<int> Counter => State<int>.Value(this, () => 0);

public async ValueTask Up(CancellationToken ct)
{
  var current = await Counter;
  Counter.Set(current + 1, ct);
}

You should instead use the Update method:

public async ValueTask Up(CancellationToken ct)
{
  Counter.Update(current => current + 1, ct);
}

UpdateMessage

This gives you the ability to update a state, including the metadata.

Note

This is the raw way to update a state and is designed for advanced usage and should probably not be used directly in apps.