Odonno / ReduxSimple

Simple Stupid Redux Store using Reactive Extensions
http://www.nuget.org/packages/ReduxSimple/
MIT License
143 stars 19 forks source link

Throw exception in reducer will cause ReduxStore stop work for new Dispatch #90

Closed RockNHawk closed 3 years ago

RockNHawk commented 3 years ago

Hi,

I've add some validation logic in reducer, and throw exception if not valid, but any exception throw will ReduxStore stop work for next action Dispatch.

It seems the inner Subject set self state to complete / error when exception hanppened in OnNext handler, and never handle new OnNext call.

RockNHawk commented 3 years ago

Does this by design ? should not throw exception in reducer ? but put validation logic in reducer is muth cleaner than outsite.

Odonno commented 3 years ago

Yes, this is by design. In what occasion would you throw an Exception in a reducer? I've never seen anything like that.

RockNHawk commented 3 years ago

For example, before adding a thing, verify whether it is exists, and throw an exception if it exists.

Odonno commented 3 years ago

Well, I can see that but I never ever did that. May you need an Effect to check the existence of the thing and then dispatch a proper action which will not break the pure reducers?

RockNHawk commented 3 years ago

@Odonno

Well, I can see that but I never ever did that. May you need an Effect to check the existence of the thing and then dispatch a proper action which will not break the pure reducers?

I've add a special logic to handle this, I wrap the Redux.Dispatch method and all the reducer method wrap with an try-catch, if an Exception throw in reducer, ·action.Exception· will set, then the wrapped Redux.Dispatchmethod whill re-throw the action.Exception.

A bit ugly, I will use a more elegant way in the future.

interface IWithExceptionAction
{
    Exception? Exception { get; set; }
}

static class ActionUtility
{
    public static void ThrowIfError<T>(this T action) where T : IWithExceptionAction
    {
        var ex = action.Exception;
        if (ex != null) 
            throw new WrappedException($"Error execute operate for '{action.GetType().Name}', {ex.Message}", ex);
    }
}

public void Dispatch<T>(T action) where T : IWithExceptionAction
{
    Store.Dispatch(action);
    action.ThrowIfError();
}

public static ReduxSimple.On<TState> OnWithEx<TAction>(Func<TState, TAction, TState> reducer) where TAction : class, IWithExceptionAction
  {
    return Reducers.On<TAction, TState>((state, action) =>
    {
        try
        {
          return reducer.Invoke(state, action);
        }
        catch (Exception e)
        {
          action.Exception = e;
          return state;
        }
    });
}

public override IEnumerable<On<FileSystemSourceState>> CreateReducers()
{
    return new[]
    {
        OnWithEx<AddFileSystemSourceAction>(OnAddFileSystemSource),
        OnWithEx<StartRemoveFileSystemSourceAction>(OnStartRemoveFileSystemSourceAction),
        OnWithEx<DoneRemoveFileSystemSourceAction>((state, payload) =>
        {
            var oldSources = state.AllSources;
            var idx = oldSources.IndexOf(x => x.Id == payload.Source.Id);

                            ** throw  Exception: **

            if (idx == -1)
                throw new InvalidOperationException($"could not find Source#{payload.Source.Id} in state.");
            var source = oldSources[idx];
            Debug.Assert(source == payload.Source);
            source.Status = DataSourceStatus.Removed;
            return state.With(
                new
                {
                    AllSources = oldSources.RemoveAt(idx),
                });
        }),
    };
}