mrpmorris / Fluxor

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

Error: SystemNullReferenceException: Have you forgotten to call base.OnInitialized() in your component? #412

Closed WhitWaldo closed 1 year ago

WhitWaldo commented 1 year ago

I'm attempting to use this in a Blazor Server project and simply cannot figure out what I'm missing that's causing it not to just work.

In program.cs:

builder.Services.AddFluxor(opt =>
{
    opt.ScanAssemblies(typeof(Program).Assembly);
    opt.UseReduxDevTools();
});

I have the <Fluxor.Blazor.Web.StoreInitializer/> at the top of my App.razor file.

ConditionComponentState.cs:

namespace ThisThat
{
    [FeatureState] //Doesn't matter if I include this or not
    public class ConditionContainerFeature : Feature<ConditionSetContainer>
    {
        public override string GetName() => "ConditionSetContainer";

        protected override ConditionSetContainer GetInitialState()
        {
            //Initialize with a condition set container comprised of one condition set containing one condition.
            return new ConditionSetContainer
            {
                ConditionSets = new List<ConditionSet>
                {
                    new ConditionSet
                    {
                        Conditions = new List<ICondition>
                        {
                            new UrlCondition()
                        }
                    }
                }
            };
        }
    }

    public class ConditionContainerInitializedAction { }

    public class ConditionContainerAddSetAction
    {
        /// <summary>
        /// The condition set to add to the container.
        /// </summary>
        public ConditionSet Set { get; }

        public ConditionContainerAddSetAction(ConditionSet set)
        {
            Set = set;
        }
    }

    public class ConditionContainerRemoveSetAction
    {
        /// <summary>
        /// The identifier of the condition set to remove from the container.
        /// </summary>
        public Guid ConditionSetId { get; }

        public ConditionContainerRemoveSetAction(Guid conditionSetId)
        {
            ConditionSetId = conditionSetId;
        }
    }

    public class ConditionContainerUpdatedAction
    {
        /// <summary>
        /// The identifier of the condition set to update the condition on.
        /// </summary>
        public Guid ConditionSetId { get; }

        /// <summary>
        /// The updated condition details.
        /// </summary>
        public UrlCondition Condition { get; }

        public ConditionContainerUpdatedAction(Guid conditionSetId, UrlCondition condition)
        {
            ConditionSetId = conditionSetId;
            Condition = condition;
        }
    }

    public static class ConditionContainerReducers
    {
        /// <summary>
        /// Handles the condition set initializing in state.
        /// </summary>
        /// <param name="state">The container state.</param>
        /// <returns></returns>
        [ReducerMethod(typeof(ConditionContainerInitializedAction))]
        public static ConditionSetContainer OnConditionSetInitialized(ConditionSetContainer state)
        {
            return state with { };
        }

        /// <summary>
        /// Handles the addition of a condition set to a container.
        /// </summary>
        /// <param name="state">The existing condition container.</param>
        /// <param name="action">Information about the new condition set to add.</param>
        /// <returns></returns>
        [ReducerMethod]
        public static ConditionSetContainer OnConditionSetAdded(ConditionSetContainer state, ConditionContainerAddSetAction action)
        {
            var updatedConditionSets = state.ConditionSets;
            updatedConditionSets.Add(action.Set);

            return state with
            {
                ConditionSets = updatedConditionSets
            };
        }

        /// <summary>
        /// Handles the removal of a condition set in a container.
        /// </summary>
        /// <param name="state">The existing condition container.</param>
        /// <param name="action">Information about the condition set to remove.</param>
        /// <returns></returns>
        [ReducerMethod]
        public static ConditionSetContainer OnConditionSetRemoved(ConditionSetContainer state, ConditionContainerRemoveSetAction action)
        {
            return state with
            {
                ConditionSets = state.ConditionSets.Where(set => set.Id != action.ConditionSetId).ToList()
            };
        }
    }
}

And finally my component:

@inherits FluxorComponent
@inject IDispatcher Dispatcher
@inject IState<ConditionSetContainer> ContainerState

<div class="relative flex flex-col justify-between rounded-sm scrollbar-thin scrollbar-thumb-sky-500 scrollbar-track-gray-900 overflow-y-auto pr-4">
    <div class="flex text-white text-sm">
        <div class="grow"></div> 
        <div>
            <button type="button" @onclick="AddConditionSetButtonClicked" class="text-md rounded-sm bg-transparent border p-1 text-sky-400 border-sky-400 shadow-sm hover:bg-sky-400 hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-500">
                <i class="fa-regular fa-plus"></i>
                Add Group
            </button>
        </div>
    </div>
    @if (ConditionSets.Any())
    {
        <div class="flex flex-col space-y-2 mt-2">
        </div>
    }
</div>

@code {
    private List<ConditionSet> ConditionSets => ContainerState.Value.ConditionSets;

    /// <summary>
    /// Handles when the user clicks the Add Condition Set button
    /// </summary>
    private void AddConditionSetButtonClicked()
    {
        //Add a new default condition set to the container
        Dispatcher.Dispatch(new ConditionContainerAddSetAction(new ConditionSet()));
    }
}

As soon as I click the button responsible for displaying this component, an exception is thrown:

System.NullReferenceException: Have you forgotten to call base.OnInitialized() in your component? at Fluxor.Blazor.Web.Components.FluxorComponent.Dispose(Boolean disposing) in C:\Data\Mine\Code\Fluxor\Source\Lib\Fluxor.Blazor.Web\Components\FluxorComponent.cs:line 84 at Fluxor.Blazor.Web.Components.FluxorComponent.Dispose() in C:\Data\Mine\Code\Fluxor\Source\Lib\Fluxor.Blazor.Web\Components\FluxorComponent.cs:line 61 at Microsoft.AspNetCore.Components.Rendering.ComponentState.TryDisposeInBatch(RenderBatchBuilder batchBuilder, Exception& exception)

Fluxor seems like the perfect fit for my state needs but I cannot for the life of me figure out why this isn't working. Can anyone shed any light on what I'm missing here?

Thank you!

mrpmorris commented 1 year ago

My guess is that the conponent is being created, then dependency injection runs in order to update your Inject properties but throws an exception, then the component is Disposed before OnInitialized has been called.

StoreInitializer has an event for unhandled exceptions

https://github.com/mrpmorris/Fluxor/blob/master/Source/Lib/Fluxor.Blazor.Web/StoreInitializer.cs#L17

Try hooking that up to see if it is triggered with an exception that can give you more information.

WhitWaldo commented 1 year ago

You're right - I scrolled up in the debug window and there was an exception throwing beforehand about a parameter being populated that didn't exist on the component. Removed that and it works like a charm. Thank you!

mrpmorris commented 1 year ago

You're welcome :)