Search code examples
c#blazorfluxblazor-webassemblyfluxor

Sharing state and handling events with 3 child components | fluxor


I'm new to fluxor and the redux/flux patterns it's based on. I'm currently working on a Blazor wasm project. I've chosen to use fluxor for state management. But I'm wondering how to handle the following scenario:

When the page gets loaded component 1 is filled with data from an API trough fluxor state management.

Component 1 is a list with items a user can select. When clicked said item should get retrieved from an API and get shown in full inside of component 3. And a text representing the active item is shown in Component 2.

Component 2 is a navigator that goes trough the list using simple back and next buttons. When clicking next then component 3 should retrieve the next item from the API and show the item in full. Component 1 then reflects this change by showing what item of the list is selected.

skribbled

I do have actions etc to get the full item. I'm just not sure where to dispatch it from. Or how to make sure all components know what the active item is. With "pure" blazor I would do this all trough event callbacks, and handle the active item state locally. but that would defeat the point of fluxor.


Solution

  • If these components are all different parts of a single use-case (to browse client data) then I'd put them all inside a single feature.

    Let's say you have the following contract classes coming from your API.

    public class ClientSummary
    {
      public Guid ID { get; set; }
      public string Name { get; set; }
    }
    
    public class ClientDetails
    {
      public Guid ID { get; set; }
      public string Name { get; set; }
      public string LotsOfOtherInfo { get; set; }
      public string ArchivedInfo { get; set; }  
    }
    
    

    Note: Personally I'd give them private setters and use Newtonsoft for deserializing API responses. This will ensure they are immutable, and you can use them directly in state without having to create twin classes just to make your state immutable.

    Your feature state might look something like this

    public class MyUseCaseState
    {
      public bool IsLoadingList { get; }
      public ReadOnlyCollection<ClientSummary> Summaries { get; }
      public int SelectedClientIndex { get; }
    
      public bool IsLoadingDetails { get; }
      public ClientDetails ClientDetails { get; }
    
      public MyUseCaseState(
        bool isLoadingList, 
        IEnumerable<ClientSummary> summaries, 
        int selectedClientIndex, 
        bool isLoadingDetails, 
        ClientDetails clientDetails)
      {
        IsLoadingList = isLoadingList;
        Summaries = (summaries ?? Enumerable.Empty<ClientSummary>()).ToList().AsReadOnly();
        SelectedClientIndex = selectedClientIndex;
        IsLoadingDetails = isLoadingDetails;
        ClientDetails = clientDetails;
      }
    }
    

    The action you fire when your page is displayed doesn't need any payload

    public class LoadClientListAction {}
    

    The reducer would clear the list and set the index to -1;

    [ReducerMethod]
    public static MyUseCaseState ReduceLoadClientListAction(MyUseCaseState state, LoadClientListAction action)
    => new MyUseCaseState(
         isLoadingList: true,
         summaries: null,
         selectedClientIndex: -1,
         isLoadingDetails: false,
         clientDetails: null
       );
    

    Your effect would go off to the server to grab the list, and then push to state via an action

    [EffectMethod]
    public async Task HandleLoadClientListAction(LoadClientListAction action, IDispatcher dispatcher)
    {
      ClientSummary[] clientSummaries = await GetFromApi.....;
      ClientSummary firstClient = clientSummaries.FirstOrDefault();
      var result = new LoadClientListResultAction(clientSummaries, firstClient);
      dispatcher.Dispatch(result);
    }
    

    Your reducer method for this action

    [ReducerMethod]
    public static MyUseCaseState ReduceLoadClientListResultAction(MyUseCaseState state, LoadClientListResultAction action)
    => new MyUseCaseState(
         isLoadingList: false,
         summaries: action.Summaries,
         selectedClientIndex: action.Summaries.Count == 0 ? -1 : 0,
         isLoadingDetails: false,
         clientDetails: action.FirstClient
       );
    

    Now you need to load data for the selected client whenever the SelectedClientIndex changes, or the list loads. You can have a single action just to set the selected index.

    @inject IState<MyUseCaseState> MyUseCaseState
    
    Dispatcher.Dispatcher(new SelectIndexAction(MyUseCaseState.Value.SelectedClientIndex + 1));
    

    You can now set the IsLoadingDetails to true, use an effect to fetch the data from the server (the ClientDetails), and then dispatch the result from the server to update your state.

    [ReducerMethod]
    public static MyUseCaseState ReduceSelectIndexAction(MyUseCaseState state, SelectIndexAction action)
    => new MyUseCaseState(
         isLoadingList: state.IsLoadingList,
         summaries: state.Summaries,
         selectedClientIndex: action.SelectedIndex,
         isLoadingDetails: true,
         clientDetails: null
       );
    

    The effect to get the data

    [EffectMethod]
    public async Task HandleSelectIndexAction(SelectIndexAction action, IDispatcher dispatcher)
    {
      ClientDetails details = await GetFromYourApi....
      var result = new SelectIndexResultAction(details);
      dispatcher.Dispatch(result);
    }
    

    Then finally update your state

    [ReducerMethod]
    public static MyUseCaseState ReduceSelectIndexAction(MyUseCaseState state, SelectIndexResultAction action)
    => new MyUseCaseState(
         isLoadingList: state.IsLoadingList,
         summaries: state.Summaries,
         selectedClientIndex: state.SelectedIndex,
         isLoadingDetails: false,
         clientDetails: action.ClientDetails
       );