mrpmorris / Fluxor

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

Create ISelector<TState, TValue> class #221

Closed mrpmorris closed 2 years ago

mrpmorris commented 2 years ago

Current person is @CurrentPerson.Value?.Name

@code 
{
  [Inject]
  private ISelector<MyState, Person> CurrentPerson;

  protected override OnInitialized()
  {
    CurrentPerson.Select(x => x.People[x.CurrentPersonIndex]);
  }
}
szalapski commented 2 years ago

Is this a idea after you wrote "There is currently no way to react to the change of subsections. I don't think it would be difficult to write something."

If so, I'm not sure how the above would help to rerender only on a person change rather than any change to People. Could you say a little more?

mrpmorris commented 2 years ago

The code in the example would identify a subsection of your state. When that value changes the component will have StateHasChanged called automatically.

So instead of requesting a whole feature state via IState, you are expressing an interest only in part of it.

szalapski commented 2 years ago

So in this example, the component is consuming "part" of the state in two places: in the "Current person is" text, and in the improper unassigned expression in OnInitialized? I don't quite get how CurrentPerson could both have a .Value property in line 1 and be an IEnumerable in line 10?

mrpmorris commented 2 years ago

Line 10 is State, line 1 is an object holding the result of the selection.

mrpmorris commented 2 years ago

BTW: If in your component you have a member that holds the previous value, you can override ShouldRender in your component to return false if it hasn't changed.

szalapski commented 2 years ago

OK, you are using the method name "Select" for the function that defines which portion of the State I want to depend on, then thereafter CurrentPerson.Value contains that portion of the State?

So that means before I call Select, CurrentPerson.Value is null, or throws an exception, or something like that?

BTW: If in your component you have a member that holds the previous value, you can override ShouldRender in your component to return false if it hasn't changed.

My package does exactly that in a way that maintains the previous value automatically, but it requires the consumer to implement a function that serves as a hash for the display state. I'm hoping that once I introduce real state management, I can mostly depend on whatever we come up with here instead.

szalapski commented 2 years ago

Do I understand it right? You want to use the method name "Select" for the function that defines which portion of the State I want to depend on, then thereafter CurrentPerson.Value contains that portion of the State?

So that means before I call Select, CurrentPerson.Value is null, or throws an exception, or something like that?

mrpmorris commented 2 years ago

Correct, it would throw an exception

szalapski commented 2 years ago

Okay, I'll try this out of it gets implemented. Thanks.

mrpmorris commented 2 years ago

@szalapski Could you try out the source in https://github.com/mrpmorris/Fluxor/tree/221/PM-StateSelector

  1. Inject IStateSelection<MyState, string> NameState;
  2. In OnInitialized call NameState.Select(x => x?.Name);

Then it should only trigger a component render when the name changes

szalapski commented 2 years ago

Do you have a prerelease nuget package?

On Wed, Nov 10, 2021 at 10:12 AM Peter Morris @.***> wrote:

@szalapski https://github.com/szalapski Could you try out the source in https://github.com/mrpmorris/Fluxor/tree/221/PM-StateSelector

  1. Inject IStateSelection<MyState, string> NameState;
  2. In OnInitialized call NameState.Select(x => x?.Name);

Then it should only trigger a component render when the name changes

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mrpmorris/Fluxor/issues/221#issuecomment-965497611, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAUY5MLC6DSQNUXZISQF2TLULKKWPANCNFSM5G7UM5LA .

mrpmorris commented 2 years ago

No, you'd have to add source to your project

szalapski commented 2 years ago

OK, I'll try that now.

szalapski commented 2 years ago

It appears it won't build without MrPMorris.snk. I tried manually disabling strong naming for both projects, but I still get:

3>------ Rebuild All started: Project: Fluxor.Blazor.Web, Configuration: Debug Any CPU ------
3>CSC : error CS7027: Error signing output with public key from file 'MrPMorris.snk' -- File not found.
3>Done building project "Fluxor.Blazor.Web.csproj" -- FAILED.

Would it be easier for you to provide a pre-release nuget package?

EDIT: I created my own .snk file to get past this and am trying it out.

szalapski commented 2 years ago

Yes, this works. When only part of the state changes, the components that depend only on the unchanged properties are not rerendered.

Now I am wary of all the ceremony code this approach requires. I see now how the dynamic nature of Javascript makes such maneuvers easier there, such as with Vuex or Redux. I can't help but think there should be a way to do this without configuring it in every component; rather, to deduce dependencies simply from the getters a component uses.

I think it would take aspects (e.g. using PostSharp): Could it be possible to create an aspect that runs after every state property's getter runs which would register which component has a dependency, and create a corresponding aspect for every setter in the the state object that will read those registrations and trigger a StateHasChanged in the components that are registered for the corresponding getter?

mrpmorris commented 2 years ago

How would you implement it in VueX / Redux?

szalapski commented 2 years ago

The nature of Vue means that properties on components automatically get "rewritten" to have getters and setters internally, so that the internal black-box setter of a property triggers code that makes every setter go out and reactively trigger each getter. VueX uses these properties naturally so that any mutation cascades through to all the right places and causes no processing in any components that don't use it.

In C# and Blazor, we don't have reactivity; instead, we have things that trigger a re-render. So the equivalent would be to make a reducer--or, more likely, other code in Fluxor that runs the reducer--to compare old vs new, figure out the properties that are changed, figure out the components that consume those properties, and call StateHasChanged only on the components that care about the changed property. Seems difficult.

mrpmorris commented 2 years ago

In angular rxjs the values are subscribed to when you pipe them. A change to that value causes a change only to the parts of the UI that are subscribed.

Blazor does it differently. It generates a tree in memory, diffs it with the last tree rendered, and then only renders the differences in the DOM.

There is no way to do that only for parts of a single component. The most finely grained you can get is component level. So you'd have to break your single component into many and nest them.

Writing a deep compare for Fluxor isn't an option.

If you created a record type for your view, you could Select() transform your State into one of those and then Fluxor would use the deep compare built into .NET.

It would possibly be faster than comparing a render tree when you don't need to render, but when it does need to render then Blazor will still have to diff trees anyway. So if it will save you any CPU or not, I don't know.