Skip to content

Commit b5776e0

Browse files
committed
Enhance documentation for UpdateThrottled and Getting Started
- Updated UpdateThrottled page with detailed examples, API signature, and best practices for throttling. - Added explanations for leading vs trailing edge throttling and included recommended intervals for various use cases. - Improved the Getting Started page by clarifying registration options and emphasizing the importance of required services for utilities and async actions.
1 parent 9cd6c03 commit b5776e0

6 files changed

Lines changed: 2295 additions & 120 deletions

File tree

README.md

Lines changed: 200 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,38 @@ dotnet add package EasyAppDev.Blazor.Store
7272

7373
---
7474

75+
## Required Services
76+
77+
Before using `StoreComponent<TState>` or async helpers, you **MUST** register utility services:
78+
79+
### Quick Setup (Recommended)
80+
```csharp
81+
builder.Services.AddStoreWithUtilities(
82+
new MyState(),
83+
(store, sp) => store.WithDefaults(sp, "MyStore"));
84+
```
85+
86+
### Manual Setup
87+
```csharp
88+
// 1. Register utilities (required for UpdateDebounced, UpdateThrottled, LazyLoad)
89+
builder.Services.AddStoreUtilities();
90+
91+
// 2. Register async executor (required for ExecuteAsync)
92+
builder.Services.AddAsyncActionExecutor<MyState>();
93+
94+
// 3. Register store
95+
builder.Services.AddStore(
96+
new MyState(),
97+
(store, sp) => store.WithDefaults(sp, "MyStore"));
98+
```
99+
100+
### What Each Service Provides
101+
- **`AddStoreUtilities()`**: Registers `IDebounceManager`, `IThrottleManager`, `ILazyCache` (scoped)
102+
- **`AddAsyncActionExecutor<T>()`**: Enables `ExecuteAsync` helper methods (scoped)
103+
- **`AddStoreWithUtilities()`**: One-liner that combines all registrations (recommended)
104+
105+
---
106+
75107
## Quick Start
76108

77109
**3 steps. 1 minute. Zero boilerplate.**
@@ -104,12 +136,21 @@ public record CounterState(int Count, string? LastAction = null)
104136
In your `Program.cs`:
105137

106138
```csharp
139+
// Option 1: All-in-one registration (RECOMMENDED)
140+
builder.Services.AddStoreWithUtilities(
141+
new CounterState(0),
142+
(store, sp) => store.WithDefaults(sp, "Counter"));
143+
144+
// Option 2: Manual registration (if you need more control)
145+
builder.Services.AddStoreUtilities(); // Required for async helpers
146+
builder.Services.AddAsyncActionExecutor<CounterState>(); // Required for ExecuteAsync
107147
builder.Services.AddStore(
108148
new CounterState(0),
109149
(store, sp) => store.WithDefaults(sp, "Counter"));
110150
```
111151

112-
> 💡 **Tip**: Use `WithDefaults()` for one-liner setup with JSRuntime, DevTools, and Logging!
152+
> 💡 **Tip**: Use `AddStoreWithUtilities()` for one-liner setup that includes all required services!
153+
> 💡 **Tip**: Use `WithDefaults()` for JSRuntime, DevTools, and Logging configuration!
113154
114155
### Step 3: Use in Components
115156

@@ -708,7 +749,7 @@ public record CounterState(int Count, string? LastAction = null)
708749
}
709750

710751
// Program.cs
711-
builder.Services.AddStore(
752+
builder.Services.AddStoreWithUtilities(
712753
new CounterState(0),
713754
(store, sp) => store.WithDefaults(sp, "Counter"));
714755

@@ -758,7 +799,7 @@ public record TodoState(ImmutableList<Todo> Todos)
758799
}
759800

760801
// Program.cs
761-
builder.Services.AddStore(
802+
builder.Services.AddStoreWithUtilities(
762803
TodoState.Initial,
763804
(store, sp) => store
764805
.WithDefaults(sp, "Todos")
@@ -885,7 +926,7 @@ public record AuthState(User? CurrentUser, bool IsLoading, string? Error)
885926

886927
// Program.cs
887928
builder.Services.AddScoped<IAuthService, AuthService>();
888-
builder.Services.AddStore(
929+
builder.Services.AddStoreWithUtilities(
889930
AuthState.Initial,
890931
(store, sp) => store
891932
.WithDefaults(sp, "Auth")
@@ -987,7 +1028,7 @@ public record ChartDataPoint(DateTime Time, decimal Value);
9871028
public record ServerStatus(bool IsHealthy, int ResponseTimeMs);
9881029

9891030
// Program.cs
990-
builder.Services.AddStore(
1031+
builder.Services.AddStoreWithUtilities(
9911032
DashboardState.Initial,
9921033
(store, sp) => store.WithDefaults(sp, "Dashboard"));
9931034

@@ -1511,7 +1552,7 @@ Explore the async helpers in action with our interactive demos:
15111552
Automatically save and restore state:
15121553

15131554
```csharp
1514-
builder.Services.AddStore(
1555+
builder.Services.AddStoreWithUtilities(
15151556
new AppState(),
15161557
(store, sp) => store
15171558
.WithDefaults(sp, "MyApp")
@@ -1530,7 +1571,9 @@ Time-travel debugging with Redux DevTools:
15301571
```csharp
15311572
builder.Services.AddStore(
15321573
new AppState(),
1533-
store => store.WithDevTools("MyApp"));
1574+
(store, sp) => store
1575+
.WithJSRuntime(sp.GetRequiredService<IJSRuntime>())
1576+
.WithDevTools("MyApp"));
15341577
```
15351578

15361579
**Features:**
@@ -1646,13 +1689,13 @@ public class LoggingMiddleware<TState> : IMiddleware<TState> where TState : notn
16461689

16471690
public LoggingMiddleware(ILogger<LoggingMiddleware<TState>> logger) => _logger = logger;
16481691

1649-
public Task OnBeforeUpdateAsync(TState currentState, string action)
1692+
public Task OnBeforeUpdateAsync(TState currentState, string? action)
16501693
{
16511694
_logger.LogInformation("Before: {Action}", action);
16521695
return Task.CompletedTask;
16531696
}
16541697

1655-
public Task OnAfterUpdateAsync(TState newState, string action)
1698+
public Task OnAfterUpdateAsync(TState previousState, TState newState, string? action)
16561699
{
16571700
_logger.LogInformation("After: {Action} -> {State}", action, newState);
16581701
return Task.CompletedTask;
@@ -1662,7 +1705,10 @@ public class LoggingMiddleware<TState> : IMiddleware<TState> where TState : notn
16621705
// Register
16631706
builder.Services.AddStore(
16641707
new AppState(),
1665-
store => store.WithMiddleware<LoggingMiddleware<AppState>>());
1708+
(store, sp) => {
1709+
var logger = sp.GetRequiredService<ILogger<LoggingMiddleware<AppState>>>();
1710+
return store.WithMiddleware(new LoggingMiddleware<AppState>(logger));
1711+
});
16661712
```
16671713

16681714
**Built-in middleware:**
@@ -1677,80 +1723,170 @@ builder.Services.AddStore(
16771723

16781724
### StoreComponent<TState>
16791725

1680-
Base component class with automatic subscription:
1726+
Base component class with automatic subscription and async helpers:
16811727

16821728
```csharp
16831729
public abstract class StoreComponent<TState> : ComponentBase
16841730
{
1685-
protected TState State { get; } // Current state
1686-
protected Task Update(Func<TState, TState> updater); // Update (returns Task)
1687-
protected Task UpdateAsync(Func<TState, Task<TState>> updater); // Async update
1731+
// State access
1732+
protected TState State { get; }
1733+
1734+
// Core update methods
1735+
protected Task Update(Func<TState, TState> updater, string? action = null);
1736+
protected Task UpdateAsync(Func<TState, Task<TState>> asyncUpdater, string? action = null);
1737+
1738+
// Debounced updates
1739+
protected Task UpdateDebounced(
1740+
Func<TState, TState> updater,
1741+
int delayMilliseconds,
1742+
[CallerMemberName] string? action = null);
1743+
protected Task UpdateDebouncedAsync(
1744+
Func<TState, Task<TState>> asyncUpdater,
1745+
int delayMilliseconds,
1746+
[CallerMemberName] string? action = null);
1747+
1748+
// Throttled updates
1749+
protected Task UpdateThrottled(
1750+
Func<TState, TState> updater,
1751+
int intervalMilliseconds,
1752+
[CallerMemberName] string? action = null);
1753+
protected Task UpdateThrottledAsync(
1754+
Func<TState, Task<TState>> asyncUpdater,
1755+
int intervalMilliseconds,
1756+
[CallerMemberName] string? action = null);
1757+
1758+
// Async action execution (requires IAsyncActionExecutor<TState> registration)
1759+
protected Task ExecuteAsync<TResult>(
1760+
Func<Task<TResult>> asyncAction,
1761+
Func<TState, TState> loading,
1762+
Func<TState, TResult, TState> success,
1763+
Func<TState, Exception, TState>? error = null,
1764+
[CallerMemberName] string? action = null);
1765+
1766+
// Lazy loading with caching
1767+
protected Task<T> LazyLoad<T>(
1768+
string cacheKey,
1769+
Func<Task<T>> loader,
1770+
TimeSpan? cacheFor = null);
16881771
}
16891772
```
16901773

1774+
> **Note**: `[CallerMemberName]` automatically fills the `action` parameter with the calling method name for DevTools/logging.
1775+
16911776
### SelectorStoreComponent<TState>
16921777

16931778
Optimized component with granular reactivity:
16941779

16951780
```csharp
16961781
public abstract class SelectorStoreComponent<TState> : ComponentBase
16971782
{
1783+
// State access
16981784
protected TState State { get; }
16991785
protected object? Selected { get; }
1700-
protected abstract object SelectState(TState state); // Override this
1786+
1787+
// Required: Override this to select specific state slice
1788+
protected abstract object SelectState(TState state);
1789+
1790+
// Update methods (same as StoreComponent)
1791+
protected Task Update(Func<TState, TState> updater, string? action = null);
1792+
protected Task UpdateAsync(Func<TState, Task<TState>> asyncUpdater, string? action = null);
17011793
}
17021794
```
17031795

1796+
> **Note**: `SelectorStoreComponent` does NOT include async helpers (UpdateDebounced, ExecuteAsync, LazyLoad). It's focused solely on granular re-rendering optimization. Use `StoreComponent` if you need async helpers.
1797+
17041798
### IStore<TState>
17051799

17061800
Store interface (rarely used directly):
17071801

17081802
```csharp
1709-
public interface IStore<TState>
1803+
public interface IStore<TState> :
1804+
IStateReader<TState>,
1805+
IStateWriter<TState>,
1806+
IStateObservable<TState>,
1807+
IDisposable
1808+
where TState : notnull
1809+
{
1810+
// Composed from three segregated interfaces (Interface Segregation Principle)
1811+
}
1812+
```
1813+
1814+
**Component Interfaces:**
1815+
1816+
```csharp
1817+
// Read-only state access
1818+
public interface IStateReader<TState> where TState : notnull
17101819
{
17111820
TState GetState();
1821+
}
1822+
1823+
// State update operations
1824+
public interface IStateWriter<TState> where TState : notnull
1825+
{
17121826
void Update(Func<TState, TState> updater, string? action = null);
17131827
Task UpdateAsync(Func<TState, TState> updater, string? action = null);
17141828
Task UpdateAsync(Func<TState, Task<TState>> asyncUpdater, string? action = null);
1829+
}
1830+
1831+
// Subscription management
1832+
public interface IStateObservable<TState> where TState : notnull
1833+
{
17151834
IDisposable Subscribe(Action<TState> callback);
17161835
IDisposable Subscribe<TSelected>(
17171836
Func<TState, TSelected> selector,
17181837
Action<TSelected> callback);
1838+
IDisposable Subscribe<TSelected>(
1839+
Func<TState, TSelected> selector,
1840+
Action<TSelected> callback,
1841+
IEqualityComparer<TSelected> comparer);
17191842
}
17201843
```
17211844

1845+
> **Note**: The segregated interface design allows components to depend only on the capabilities they need (SOLID principles).
1846+
17221847
### Registration
17231848

17241849
```csharp
1725-
// Simple - no configuration
1726-
services.AddStore(new MyState());
1727-
1728-
// With defaults (recommended)
1729-
services.AddStore(
1850+
// RECOMMENDED: All-in-one with utilities
1851+
builder.Services.AddStoreWithUtilities(
17301852
new MyState(),
17311853
(store, sp) => store.WithDefaults(sp, "MyStore"));
17321854

1733-
// Full configuration
1734-
services.AddStore(
1855+
// Manual registration (when you need control)
1856+
builder.Services.AddStoreUtilities(); // Registers async helper services
1857+
builder.Services.AddAsyncActionExecutor<MyState>(); // Required for ExecuteAsync
1858+
builder.Services.AddStore(
17351859
new MyState(),
1736-
(store, sp) => store
1737-
.WithDefaults(sp, "StoreName")
1738-
.WithPersistence(sp, "storage-key")
1739-
.WithDiagnosticsIfAvailable(sp)
1740-
.WithMiddleware<MyMiddleware>()
1741-
);
1860+
(store, sp) => store.WithDefaults(sp, "MyStore"));
1861+
1862+
// Simple - minimal configuration (no async helpers)
1863+
builder.Services.AddStore(new MyState());
17421864

1743-
// Manual configuration (for advanced scenarios)
1744-
services.AddStore(
1865+
// Full configuration with middleware
1866+
builder.Services.AddStoreWithUtilities(
17451867
new MyState(),
1746-
(store, sp) => store
1747-
.WithJSRuntime(sp.GetRequiredService<IJSRuntime>())
1748-
.WithDevTools("StoreName")
1749-
.WithLogging()
1750-
.WithMiddleware<MyMiddleware>()
1751-
);
1868+
(store, sp) => {
1869+
var customMiddleware = new MyCustomMiddleware<MyState>();
1870+
return store
1871+
.WithDefaults(sp, "StoreName")
1872+
.WithPersistence(sp, "storage-key")
1873+
.WithDiagnosticsIfAvailable(sp)
1874+
.WithMiddleware(customMiddleware); // Instance, not generic!
1875+
});
1876+
1877+
// Scoped stores (Blazor Server)
1878+
builder.Services.AddScopedStoreWithUtilities(
1879+
new SessionState(),
1880+
store => store.WithLogging());
1881+
1882+
// Scoped with factory
1883+
builder.Services.AddScopedStoreWithUtilities(
1884+
sp => new UserState(sp.GetRequiredService<IUserContext>().UserId),
1885+
store => store.WithLogging());
17521886
```
17531887

1888+
> **Important**: Use `WithMiddleware(instance)` with a middleware **instance**, NOT `WithMiddleware<T>()` - the generic method doesn't exist!
1889+
17541890

17551891
---
17561892

@@ -1996,6 +2132,33 @@ Update(s => s.LoadDataAsync(service)); // Returns Task!
19962132
await UpdateAsync(async s => await s.LoadDataAsync(service));
19972133
```
19982134

2135+
### ExecuteAsync not available / throwing InvalidOperationException?
2136+
2137+
**Error:** `IAsyncActionExecutor<MyState> is not registered`
2138+
2139+
**Solution:** Register the async executor before using `ExecuteAsync`:
2140+
2141+
```csharp
2142+
// Option 1: All-in-one (recommended)
2143+
builder.Services.AddStoreWithUtilities(
2144+
new MyState(),
2145+
(store, sp) => store.WithDefaults(sp, "MyStore"));
2146+
2147+
// Option 2: Manual registration
2148+
builder.Services.AddAsyncActionExecutor<MyState>();
2149+
builder.Services.AddStore(new MyState(), ...);
2150+
```
2151+
2152+
### UpdateDebounced/UpdateThrottled/LazyLoad not available?
2153+
2154+
**Solution:** Register utility services:
2155+
2156+
```csharp
2157+
builder.Services.AddStoreUtilities(); // Required!
2158+
// OR
2159+
builder.Services.AddStoreWithUtilities(...); // Includes utilities
2160+
```
2161+
19992162
---
20002163

20012164
## Documentation

0 commit comments

Comments
 (0)