mrpmorris / Fluxor

Fluxor is a zero boilerplate Flux/Redux library for Microsoft .NET and Blazor.
MIT License
1.22k stars 139 forks source link

Chaining actions #450

Open massimotomasi opened 9 months ago

massimotomasi commented 9 months ago

Hello, in my use case i have to dispatch multiple actions that modifify the same state slice. How can i chain actions forcing them to be executed in a specific order?

If i try to dispatch both actions like so

Dispatcher.Dispatch(new Action1());
Dispatcher.Dispatch(new Action2());

both reducers methods (one for action) receive the same store instance wich isn't updated according to the first action's execution causing the lost of one action.

I know that i can create one ad hoc action that can manage both changes in the reduce method but i want to know if i have an alternative wich let me keep both actions separated.

Thx

andrevlins commented 9 months ago

Wouldn't it be a case of creating an effect that reacts to Action1 and triggers Action2?

massimotomasi commented 9 months ago

Hi @andrevlins this could be another option but i don't want to couple the two actions and i don't need Action2 to be fired every time i fire Action1. I have some logic that decide if Action2 should be fired after Action1 and i want that logic to stick in the razor page code behind to not couple the two actions

mrpmorris commented 8 months ago

@massimotomasi The code you wrote should dispatch the actions sequentially. Do you have a case where this is not happening?

Is there a repro you can share, perhaps a unit test?

massimotomasi commented 8 months ago

@massimotomasi The code you wrote should dispatch the actions sequentially. Do you have a case where this is not happening?

Is there a repro you can share, perhaps a unit test?

@mrpmorris Yes, it is executed sequentially but it doesn't guarantee that at the moment i handle Action2, Action1 has been completely handled and the state, wich is passed to Actions2 is updated accordingly. The example i had was a page with a tab item inside where Action1 was OnTabChangedAction (=> update the CurrentTab property of the store) and Action2 was LoadDataFromWebServiceAction (=> Load data online using an effect and show the results)

mrpmorris commented 8 months ago

All reducers should have finished for Action1 before Action2 is processed.

Note that async effects are executed in the background and night not have completed before Action2 is dispatched.

If this isn't an Effect issue but simply reducers, please let me have a repro

uhfath commented 8 months ago

@massimotomasi in your case I would just make something like FinishedEvent that is triggered by an effect of Action when it's finished. And in razor pages I would SubscribeToAction to this FinishedEvent. But if you plan to make such routine often (i.e. dispatch one action right after another) it could be better to create a simple disposable class that holds a TaskCompletionSource and calls it's SetResult when an 'event' is fired. And in user code you would just await it. Something like this:

public class AsyncDispatchBuilder : IDisposable
{
    private readonly IDispatcher _dispatcher;
    private readonly IActionSubscriber _actionSubscriber;
    private readonly TaskCompletionSource _dispatchCompletionSource = new();

    private bool _hasEvents;
    private bool _isDisposed;

    protected virtual void Dispose(bool disposing)
    {
        if (!_isDisposed)
        {
            if (disposing)
            {
                _actionSubscriber.UnsubscribeFromAllActions(this);
            }

            _isDisposed = true;
        }
    }

    public AsyncDispatchBuilder(
        IDispatcher dispatcher,
        IActionSubscriber actionSubscriber)
    {
        this._dispatcher = dispatcher;
        this._actionSubscriber = actionSubscriber;
    }

    public AsyncDispatchBuilder SubscribeToAction<TAction>(Action<TAction> callback = null)
        where TAction : IStateAction
    {
        _actionSubscriber.SubscribeToAction<TAction>(this, ac =>
        {
            callback?.Invoke(ac);
            _dispatchCompletionSource.SetResult();
        });

        _hasEvents = true;
        return this;
    }

    public Task DispatchActionAsync<TAction>(TAction stateAction)
        where TAction : IStateAction
    {
        if (!_hasEvents)
        {
            throw new ArgumentException("No events were subscribed");
        }

        _dispatcher.DispatchAction(stateAction);
        return _dispatchCompletionSource.Task;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

This could be used like:

using var builder = new AsyncDispatchBuilder(Dispatcher, ActionSubscriber)
    .SubscribeToAction<Action1.FinishedEvent>()
;

await builder.DispatchActionAsync(new Action2());

But to make it complete there should be at least 2 more 'events' - OnException and OnCancelation. Those should trigger SetException and SetCanceled of _dispatchCompletionSource.