2

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.

Peter Morris
  • 20,174
  • 9
  • 81
  • 146
Rocco
  • 457
  • 5
  • 23

1 Answers1

2

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
   );
Peter Morris
  • 20,174
  • 9
  • 81
  • 146
  • Thanks! This should work. If I may ask why did you put ClientDetails within the same feature too? – Rocco Jun 16 '20 at 09:14
  • 1
    Because it's part of the same use case. The use case is "I want to see a list of clients. I want to see the first client selected. I want to be able to navigate next/prev client, and for the details to be fetched from the server and be displayed" When you navigate away from this page, all of the state in that feature can be cleared down safely because you *know* it's no longer being used. If the ClientState is somewhere shared then you can never know it's safe to clear it down. It's better to duplicate state based on use case than to share state, because that leads to memory leakage. – Peter Morris Jun 16 '20 at 10:53
  • 3
    #1 error in apps writen in the Flux/Redux style (in my opinion) is people sharing state. Because you don't know when you can clear down that shared state, you end up never clearing it down. Your memory consumption just keeps growing - and you have to implement some way for the server to tell you which clients other users have deleted so you don't keep deleted objects in your state too. Better to duplicate data per use-case so each use-case has its own state and can be cleared down. It's reducers can react to other feature's actions to remain consistent. – Peter Morris Jun 16 '20 at 10:55