Search code examples
c#asp.net-coreblazor-webassemblyfluxor

How do we await the result of an Action/Effect in an ItemProviderDelegate?


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:

  1. 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).

  2. 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?

  • Edit - The flow of the action looks something like this:
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')

Solution

  • I think you could do something like this

    1. Have a TaskCompletionSource<ItemsProviderResult<Employee>> member in your component.
    2. In the LoadDtosAsync dispatch an action that has a property containing a reference to that TaskCompletionSource
    3. 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).