mrpmorris / Fluxor

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

Updating a selected item from a list also updates the record in the list #408

Closed vincejairam closed 1 year ago

vincejairam commented 1 year ago

I have a grid list list view where the the user clicks on a row (I fire a dispatch action for the selected item). In the grid view I also have a child component for a model component which is also has the same state to show. I am subscribing to the "set" action oninit which then sets the data for the component. It seems that when I update the view in the popmodel which triggers another action it is ALSO updating the list item

This is my store

using Fluxor;
using SPOA.Services.System;
using SPOA.Shared.Models.System.Hospital;
using SPOA.Shared.Models.User;

namespace SPOA.Portal.Store
{
    #region States

    public record HospitalState
    {
        public HospitalList HospitalLists { get; init; }
        public SelectedHospital Selected { get; init; }
    }

    #region HospitalList
    public record HospitalList
    {
        public HospitalListQuery Filter { get; set; }
        public HospitalListResponse Data { get; set; }

        public bool IsInitialized { get; set; }

        public bool IsLoading { get; set; }

        public bool HasError { get; set; }
    }
    #endregion

    #region SelectedHospital
    public class SelectedHospital
    {
        public bool ShowSelected { get; set; }
        public bool IsSubmitting { get; set; }
        public bool IsSubmitted { get; set; }
        public bool HasError { get; set; }
        public string StatusMessage { get; set; }
        public HospitalListEntry Data { get; set; }

    }
    #endregion

    #endregion

    #region Features

    public class HospitalFeature : Feature<HospitalState>
    {
        public override string GetName() => "Hospital";

        protected override HospitalState GetInitialState()
        {
            return new HospitalState
            {
                HospitalLists = new HospitalList()
                {
                    IsInitialized = false,
                    IsLoading = false,
                    HasError = false,
                    Filter = new(),
                    Data = new HospitalListResponse() { CurrentPage = 0, TotalCount = 0, Hospitals = Array.Empty<HospitalListEntry>() }
                },

                Selected = new SelectedHospital()
                {
                    ShowSelected = false,
                    IsSubmitting = false,
                    HasError = false,
                    StatusMessage = "",
                    Data = new()
                }
            };
        }
    }
    #endregion

    #region Actions

    #region HospitalList
    public record HospitalListSetInitializedAction { }

    public record HospitalListFetchDataAction
    {
        public HospitalListQuery Filter { get; init; }

        public HospitalListFetchDataAction(HospitalListQuery filter)
        {
            Filter = filter;
        }
    }

    public record HospitalListSetDataAction
    {
        public HospitalListResponse Data { get; init; }

        public HospitalListSetDataAction(HospitalListResponse data)
        {
            Data = data;
        }
    }
    #endregion

    #region SelectedHospital
    public record SelectedHospitalSetDataAction
    {
        public HospitalListEntry Data { get; init; }
        public bool ShowSelected { get; init; }

        public SelectedHospitalSetDataAction(HospitalListEntry data, bool showSelected)
        {
            Data = data;
            ShowSelected = showSelected;
        }
    }

    public record SelectedHospitalOperationAction
    {
        public HospitalListEntry Data { get; init; }
        public bool ShowSelected { get; init; }
        public string StatusMessage { get; init; }
        public bool HasError { get; init; }

        public SelectedHospitalOperationAction(HospitalListEntry data, bool showSelected, String message, bool hasError)
        {
            Data = data;
            ShowSelected = showSelected;
            StatusMessage = message;
            HasError = hasError;
        }
    }

    public record SelectedHospitalInsertSuccessAction 
    {   
        public HospitalListEntry Data { get; init; }
        public bool ShowSelected { get; init; }
        public string StatusMessage { get; init; }

        public SelectedHospitalInsertSuccessAction(HospitalListEntry data, bool showSelected, String message)
        {
            Data = data;
            ShowSelected = showSelected;
            StatusMessage = message;
        }
    }

    public record SelectedHospitalInsertFailureAction
    {
        public string StatusMessage { get; init; }
        public bool ShowSelected { get; init; }
        public SelectedHospitalInsertFailureAction(bool showSelected, string message)
        {
            ShowSelected = showSelected;
            StatusMessage = message;
        }
    }

    public record SelectedHospitalInsertAction
    {
        public HospitalListEntry Data { get; init; }
        public SPOAUser CurrentUser { get; init; }

        public SelectedHospitalInsertAction(HospitalListEntry data, SPOAUser currentUser)
        {
            Data = data;
            CurrentUser = currentUser;
        }
    }

    public record SelectedHospitalUpdateSuccessAction
    {
        public HospitalListEntry Data { get; init; }
        public bool ShowSelected { get; init; }
        public string StatusMessage { get; init; }

        public SelectedHospitalUpdateSuccessAction(HospitalListEntry data, bool showSelected, String message)
        {
            Data = data;
            ShowSelected = showSelected;
            StatusMessage = message;
        }
    }

    public record SelectedHospitalUpdateFailureAction
    {
        public string StatusMessage { get; init; }
        public bool ShowSelected { get; init; }
        public SelectedHospitalUpdateFailureAction(bool showSelected, string message)
        {
            ShowSelected = showSelected;
            StatusMessage = message;
        }
    }

    public record SelectedHospitalUpdateAction
    {
        public HospitalListEntry Data { get; init; }
        public SPOAUser CurrentUser { get; init; }

        public SelectedHospitalUpdateAction(HospitalListEntry data, SPOAUser currentUser)
        {
            Data = data;
            CurrentUser = currentUser;
        }
    }
    #endregion

    #endregion

    #region Reducers

    #region HospitalList
    public static class HospitalListReducers
    {
        [ReducerMethod]
        public static HospitalState OnSetData(HospitalState state, HospitalListSetDataAction action)
        {
            state.HospitalLists.Data = action.Data;
            state.HospitalLists.IsLoading = false;
            state.HospitalLists.HasError = action.Data == null;

            return state with
            { 
                 HospitalLists = state.HospitalLists,
            };
        }

        [ReducerMethod(typeof(HospitalListSetInitializedAction))]
        public static HospitalState OnSetInitialized(HospitalState state)
        {
            var hospitalLists = state.HospitalLists;
            hospitalLists.IsInitialized = true;
            hospitalLists.HasError = false;

            return state with
            {
                HospitalLists = hospitalLists
            };
        }

        [ReducerMethod]
        public static HospitalState OnFetchData(HospitalState state, HospitalListFetchDataAction action)
        {
            var hospitalLists = state.HospitalLists;
            hospitalLists.HasError = false;
            hospitalLists.IsLoading = true;
            hospitalLists.Filter = action.Filter;
            hospitalLists.IsInitialized = true;

            return state with
            {
                HospitalLists = hospitalLists
            };
        }
    }
    #endregion

    #region SelectedHospital
    public static class SelectedHospitalReducers
    {
        [ReducerMethod]
        public static HospitalState OnSetData(HospitalState state, SelectedHospitalSetDataAction action)
        {
            var selected = state.Selected;
            selected.Data = action.Data;
            selected.ShowSelected = action.ShowSelected;
            selected.IsSubmitting = false;
            selected.IsSubmitted = true;
            selected.HasError = false;
            selected.StatusMessage = "";

            return state with
            {
                Selected = state.Selected
            };
        }

        [ReducerMethod]
        public static HospitalState OnSetData(HospitalState state, SelectedHospitalOperationAction action)
        {
            var selected = state.Selected;
            selected.Data = action.Data;
            selected.ShowSelected = action.ShowSelected;
            selected.IsSubmitting = false;
            selected.IsSubmitted = true;
            selected.HasError = action.HasError;
            selected.StatusMessage = action.StatusMessage;

            return state with
            {
                Selected = state.Selected
            };
        }        

        [ReducerMethod]
        public static HospitalState OnHospitalUpdate(HospitalState state, SelectedHospitalUpdateAction action)
        {
            var selected = state.Selected;
            selected.IsSubmitting = true;

            return state with
            {
                Selected = selected
            };
        }

        [ReducerMethod]
        public static HospitalState OnHospitalInsert(HospitalState state, SelectedHospitalInsertAction action)
        {
            var selected = state.Selected;
            selected.IsSubmitting = true;

            return state with
            {
                Selected = selected
            };
        }

    }

    #endregion

    #endregion

    #region Effects
    public class HospitalEffects
    {
        private readonly ISystemService LookupService;
        private readonly IState<HospitalState> HospitalState;

        public HospitalEffects(ISystemService lookupService, IState<HospitalState> hospitalState)
        {
            LookupService = lookupService;
            HospitalState = hospitalState;
        }

        [EffectMethod]
        public async Task LoadListData(HospitalListFetchDataAction action, IDispatcher dispatcher)
        {
            var response = await LookupService.GetHospitalListAsync(action.Filter);
            dispatcher.Dispatch(new HospitalListSetDataAction(response));
        }

        [EffectMethod]
        public async Task AddNewHospital(SelectedHospitalInsertAction action, IDispatcher dispatcher)
        {
            int? id = await LookupService.AddHospitalAsync(action.Data, action.CurrentUser);

            if (id.HasValue)
            {
                action.Data.Id = id.Value;
                dispatcher.Dispatch(new SelectedHospitalOperationAction(action.Data, false, "System Hospital successfully added", false));
            }
            else
            {
                dispatcher.Dispatch(new SelectedHospitalOperationAction(new HospitalListEntry(), false, "Unable to add system Hospital", true));
            }
        }

        [EffectMethod]
        public async Task UpdateHospital(SelectedHospitalUpdateAction action, IDispatcher dispatcher)
        {
            bool opResult = await LookupService.UpdateHospitalAsync(action.Data, action.CurrentUser);

            if (opResult)
            {
                dispatcher.Dispatch(new SelectedHospitalOperationAction(action.Data, false, "System hospital successfully updated", false));
            }
            else
            {
                dispatcher.Dispatch(new SelectedHospitalOperationAction(new HospitalListEntry(), false, "Unable to update system hospital", true));
            }
        }
    }
    #endregion
}

issue

vincejairam commented 1 year ago

This is a snippet in my detail component

        protected override void OnInitialized()
        {
            Selected = HospitalState.Value.Selected.Data;
            SubscribeToAction<SelectedHospitalSetDataAction>(SetSelected);
            base.OnInitialized();
        }

        private void SetSelected(SelectedHospitalSetDataAction action)
        {
            Selected = action.Data;
        }

        /// <summary>
        /// Callback fired when the form submitted is valid
        /// </summary>
        /// <param name="editContext"></param>
        protected async Task OnFinishAsync(EditContext editContext)
        {
            var currentUser = await UtilityHelper.GetCurrentUserInfo(AuthenticationStateTask);

            if (Selected.Id == 0)
            {
                Dispatcher.Dispatch(new SelectedHospitalInsertAction(Selected, currentUser));
            }
            else
            {
                Dispatcher.Dispatch(new SelectedHospitalUpdateAction(Selected, currentUser));
            }
        }
vincejairam commented 1 year ago

The HospitalListResponse has paging information etc...

    public class HospitalListResponse : PagedResponse
    {
        public IEnumerable<HospitalListEntry> Hospitals { get; set; }
    }
mrpmorris commented 1 year ago

You'll have to put breakpoints on your reducers and see what changes it. I'm afraid I don't have time to debug other people's code, sorry.

vincejairam commented 1 year ago

The issue was when setting selected it still referred to the list item. I changed the action to construct a new item e.g.

    public record SelectedHospitalSetDataAction
    {
        public HospitalListEntry Data { get; init; }
        public bool ShowSelected { get; init; }

        public SelectedHospitalSetDataAction(HospitalListEntry data, bool showSelected)
        {
//create new object from orig
            var jsonData = JsonSerializer.Serialize(data);

            Data = JsonSerializer.Deserialize<HospitalListEntry>(jsonData);
            ShowSelected = showSelected;
        }
    }

Not sure if this is the recommend way or should I do this prior to dispatching, but this corrected the issue..