AsyncData<T>
Simple async state management - Eliminate 95% of loading/error boilerplate
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 = falseIsLoading = falseHasError = false |
AsyncData<User>.NotAsked() |
Loading |
Request in progress | HasData = falseIsLoading = trueHasError = false |
state.User.ToLoading() |
Success |
Data loaded successfully | HasData = trueIsLoading = falseData = T |
AsyncData<User>.Success(user) |
Failure |
Error occurred | HasData = falseIsLoading = falseHasError = trueError = 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
ExecuteAsynchelper - β Testable - Pure state transformations, easy to unit test