95% Code Reduction: 20+ lines of loading/error state management β†’ 1 property

What is AsyncData<T>?

AsyncData<T> is a discriminated union type that represents the four possible states of async operations:

⏸️

NotAsked

Initial state - no request made yet

⏳

Loading

Request in progress

βœ…

Success

Data loaded successfully

❌

Failure

Error occurred during loading

Instead of managing multiple boolean flags and nullable properties, you get type-safe state management with built-in pattern matching.

Before & After

❌ Before: 20+ lines
// State definition
public record UserState(
    User? User,
    bool IsLoading,
    string? Error)
{
    public UserState StartLoading() =>
        this with { IsLoading = true };

    public UserState Success(User u) =>
        this with { User = u, IsLoading = false };

    public UserState Failure(string e) =>
        this with { Error = e, IsLoading = false };
}

// Component usage
@if (State.IsLoading)
{
    <p>Loading...</p>
}
else if (State.Error != null)
{
    <p>Error: @State.Error</p>
}
else if (State.User != null)
{
    <p>@State.User.Name</p>
}
βœ… After: 1 property
// State definition
public record UserState(AsyncData<User> User);

// Component usage
@if (State.User.IsLoading)
{
    <p>Loading...</p>
}
@if (State.User.HasError)
{
    <p>Error: @State.User.Error</p>
}
@if (State.User.HasData)
{
    <p>@State.User.Data!.Name</p>
}

Understanding the States

State When Properties Example
NotAsked Initial state, before any request HasData = false
IsLoading = false
HasError = false
AsyncData<User>.NotAsked()
Loading Request in progress HasData = false
IsLoading = true
HasError = false
state.User.ToLoading()
Success Data loaded successfully HasData = true
IsLoading = false
Data = T
AsyncData<User>.Success(user)
Failure Error occurred HasData = false
IsLoading = false
HasError = true
Error = string
AsyncData<User>.Failure("Not found")

Complete User Profile Example

Here's a real-world authentication flow with loading states, error handling, and data display:

State.cs
public record User(string Id, string Name, string Email);

public record AuthState(AsyncData<User> CurrentUser)
{
    public static AuthState Initial => new(AsyncData<User>.NotAsked());
    public bool IsAuthenticated => CurrentUser.HasData;
}
Login.razor
@page "/login"
@inherits StoreComponent<AuthState>
@inject IAuthService AuthService

@if (State.CurrentUser.IsLoading)
{
    <p>Logging in...</p>
}
else if (State.IsAuthenticated)
{
    <p>Welcome, @State.CurrentUser.Data!.Name!</p>
    <button @onclick="Logout">Logout</button>
}
else
{
    <input @bind="email" placeholder="Email" />
    <input @bind="password" type="password" />
    <button @onclick="Login">Login</button>

    @if (State.CurrentUser.HasError)
    {
        <p class="error">@State.CurrentUser.Error</p>
    }
}

@code {
    string email = "", password = "";

    async Task Login() =>
        await ExecuteAsync(
            () => AuthService.LoginAsync(email, password),
            loading: s => s with { CurrentUser = s.CurrentUser.ToLoading() },
            success: (s, user) => s with { CurrentUser = AsyncData<User>.Success(user) },
            error: (s, ex) => s with { CurrentUser = AsyncData<User>.Failure(ex.Message) }
        );

    async Task Logout() => await Update(s => AuthState.Initial);
}

API Reference

Creating AsyncData<T>

// Initial state (no request made)
var data = AsyncData<User>.NotAsked();

// Loading state
var loading = data.ToLoading();
// Or explicitly
var loading = AsyncData<User>.Loading();

// Success state with data
var success = AsyncData<User>.Success(user);

// Failure state with error message
var failure = AsyncData<User>.Failure("User not found");

Checking State

// Check state
bool isNotAsked = data.IsNotAsked;
bool isLoading = data.IsLoading;
bool hasData = data.HasData;
bool hasError = data.HasError;

// Access data (only when HasData is true)
User user = data.Data!;  // Nullable, use with HasData check

// Access error message (only when HasError is true)
string error = data.Error;  // Empty string if no error

Pattern Matching

// In components
@if (State.User.IsNotAsked) { <p>Click to load</p> }
@if (State.User.IsLoading) { <p>Loading...</p> }
@if (State.User.HasError) { <p>Error: @State.User.Error</p> }
@if (State.User.HasData) { <p>Welcome @State.User.Data!.Name</p> }

// In state methods (match pattern)
string status = data switch
{
    { IsNotAsked: true } => "Not loaded",
    { IsLoading: true } => "Loading...",
    { HasError: true } => $"Error: {data.Error}",
    { HasData: true } => $"Loaded: {data.Data!.Name}",
    _ => "Unknown"
};

Common Patterns

1. Load on Mount

@code {
    protected override async Task OnInitializedAsync()
    {
        if (State.User.IsNotAsked)
        {
            await LoadUser();
        }
    }

    async Task LoadUser() =>
        await ExecuteAsync(
            () => UserService.GetCurrentUserAsync(),
            loading: s => s with { User = s.User.ToLoading() },
            success: (s, user) => s with { User = AsyncData.Success(user) }
        );
}

2. Retry on Error

@if (State.Data.HasError)
{
    <p>@State.Data.Error</p>
    <button @onclick="Retry">Retry</button>
}

@code {
    async Task Retry() => await Update(s => s with { Data = s.Data.ToLoading() });
}

3. Refresh Data

async Task Refresh()
{
    // Set to loading (shows spinner even if data exists)
    await Update(s => s with { User = s.User.ToLoading() });

    // Reload data
    await ExecuteAsync(
        () => UserService.GetUserAsync(userId),
        success: (s, user) => s with { User = AsyncData.Success(user) }
    );
}

4. Multiple Async Fields

public record DashboardState(
    AsyncData<User> User,
    AsyncData<ImmutableList<Post>> Posts,
    AsyncData<Stats> Stats)
{
    public static DashboardState Initial => new(
        AsyncData<User>.NotAsked(),
        AsyncData<ImmutableList<Post>>.NotAsked(),
        AsyncData<Stats>.NotAsked()
    );
}

// Component can check each independently
@if (State.User.HasData && State.Posts.HasData)
{
    // Render when both loaded
}

Benefits

  • βœ… Type-safe - Compiler enforces correct state handling
  • βœ… Impossible states impossible - Can't have loading + error simultaneously
  • βœ… 95% less boilerplate - One property vs 20+ lines
  • βœ… Better UX - Easy to show loading spinners, error messages, retry buttons
  • βœ… Composable - Works perfectly with ExecuteAsync helper
  • βœ… Testable - Pure state transformations, easy to unit test

Next Steps