In my razor component, I am using a Virtualize
component (docs here) with an ItemsProviderDelegate
which is implemented as an async
method to load Data Transfer Objects (DTOs) in batches from an API. The method looks something like this:
private async ValueTask<ItemsProviderResult<Dto>> LoadDtosAsync(ItemsProviderRequest request)
{
// Massage parameters and dispatch action
// _stateFacade is essentially a wrapper around
// Dispatcher.Dispatch(new LoadDtosAction())
_stateFacade.LoadDtos(request.StartIndex, request.Count);
// I make the assumption here that 'IsLoading' is immediately
// 'true' after the LoadDtosAction is dispatched. I think
// that this is probably a bad assumption because
// Dispatcher.Dispatch() is probably not synchronous
// under-the-hood.
// dtoState is an IState<Dto>
while(_dtoState.Value.IsLoading)
{
// My current 'solution' to wait for the data to be loaded.
await Task.Delay(100);
}
// Provide the items to the Virtualize component for rendering...
return new ItemsProviderResult<Dto>(
_dtoState.Value.VirtualizedDtos ?? new List<Dto>(),
_dtoState.Value.DtosServerCount ?? 0
);
}
This has proven to be an effective approach to rendering batches of data from collections of models in the backend which might be very large while keeping request sizes small. The client application only needs to request a handful of objects from the API at a time, while the UI does not require silly "page" controls as the user can intuitively scroll through components which display the data.
Fluxor is used to manage the state of the client application, including the current DTOs which have been requested by the Virtualize
component. This abstracts away the logic to request batches of DTOs from the API and allows side effects to be triggered depending on which component dispatches the action.
Many of the Action
types in the app have an object? Sender
property which contains a reference to the component which dispatched the action. This approach works when the original method in the component which dispatched the desired action does not require the resulting state from an action to return. Effects can then invoke callback methods depending on the type of the component sending the action, for example:
public class UpdateDtoEffect : Effect<UpdateDtoSuccessAction>
{
protected override async Task HandleAsync(UpdateDtoSuccessAction action, IDispatcher dispatcher)
{
var updateDtoForm = action.Sender as UpdateDtoForm;
if (updateDtoForm is not null)
{
await updateDtoForm.OnSuccessfulUpdate.InvokeAsync();
}
}
}
When OnSuccessfulUpdate
is invoked by the above effect, the reducer for this action will have updated the state so the callback method can rely on up-to-date state information.
An ItemsProviderDelegate
has posed an interesting exception to this approach. In order to correctly implement the delegate, we need to return the list of items and the count of how many items are available on the server. This information is stored in the state for this feature which is updated by a reducer when the LoadDtosAction
is successful. In the current implementation (generically expressed above), the LoadDtosAsync
method makes 2 assumptions that I don't like:
That the state value isLoading
is immediately set to true upon the LoadDtosAction
being dispatched. I don't think this is always true, and so the component is sometimes immediately interrogating the state value to update itself (which will result in displaying the previous state as opposed to the resulting state).
That the resulting action-reducer-effect chain will eventually update the state isLoading
value to false
.
Is there an approach that will allow the ItemsProviderDelegate
implementation to dispatch the LoadDtosAction
and "await" the results of the action to return the ItemsProviderResult
?
LoadDtosAction =>
LoadDtosActionReducer (new state, 'isLoading':true) =>
LoadDtosActionEffect (performs asynchronous API call) =>
LoadDtosSuccessAction =>
LoadDtosSuccessActionReducer (new state, 'VirtualizedDtos':{IEnumerable<Dto>}, 'DtosServerCount':{int})
LoadDtosSuccessEffect (perform optional asynchronous callbacks to 'Sender')
I think you could do something like this
TaskCompletionSource<ItemsProviderResult<Employee>>
member in your component.LoadDtosAsync
dispatch an action that has a property containing a reference to that TaskCompletionSource
await
that TaskCompletionSource
That's the UI side done, now the store part
[ReducerMethod(typeof(LoadDtosAction))]
public static MyState ReduceLoadDtosAction(MyState state) => state with {IsLoading = true };
[ReducerMethod(typeof(LoadDtosActionResult))]
public static MyState ReduceLoadDtosActionResult(MyState state) = state with {IsLoading = false; }
[EffectMethod]
public async Task HandleLoadDtosAsync(LoadDtosAction action, IDispatcher dispatcher)
{
var yourData = await HttpClient.GetJson(............);
action.TaskCompletionSource.SetResult(yourData);
Dispatcher.Dispatch(new LoadDtosActionResult()); // Just to set IsLoading = false;
}
Note that this is okay because although the TaskCompletionSource
can be considered mutable state, we aren't storing it in the Store
itself - we are just passing it around in the state of an action (which can hold mutable data).