-
Notifications
You must be signed in to change notification settings - Fork 87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Future of Redux.NET / version 3.0 #51
Comments
I would also add that if we do implement these features we separate them out similar to how I have to help with maintenance. You can see how I set up the partial classes to keep the code corresponding to the different public api methods in their own files. The part I don't know about is how others feel using something like this. I chose to keep the feel of redux in my public api since i have the experience with redux in javascript. By that I mean that I opted for the |
First, let me state clearly how I stand:
With this in mind, here's my feedback:
In short, I'm happy to discuss implementation details, but as for actually maintaining the code, that would first and foremost require Redux.NET to be as easy and simple to use for my purposes as Yarni, otherwise I'll probably just use that. |
I can see that we may each have differing goals with respect to how we would prefer to integrate this pattern into .net apps. Like @cmeeren, I lean toward an approach more native to C#, as opposed to trying to reimplement the Javascript API. To that end, having redux based on events seems reasonable, but the reason the pattern is appealing in the first place is because of how nicely it interplays with Rx. I'm really only interested in Redux+Rx, so there'd need to still be a good story there. I don't have a strong opinion about your other points, but do have thoughts on whether to include For my own purposes, I've been using a simple higher order function to define reducers in terms of typed substructure. // Register handler which target a BleState substructure and map them to the right part of State.
private static void Register<T>(AppStore store, RefAction<BleState, T> bleHandler) where T : IEvent
{
store.RegisterHandler((ref State state, T evt) => bleHandler(ref state.BleState, evt));
} Lenses provide a general solution to the problem of reading/writing substructure in statically typed functional languages without the need for reflection. Given that redux is fundamentally a functional pattern, I think this is a fruitful place to look for inspiration on this front. My example above is simply targeted at how to write reducers focused on a substructure, but there's an option to go further to make reducers easier to write as well. One goal I have is setting up a batteries-included, prepackaged "starter kit" we can use for xamarin and WPF apps. I see a redux-like pattern as part of the solution, but my focus is more on how the library API and principles will facilitate an architecture, as opposed to e.g. developing a very extensible library. In particular, I'm keen to have good solutions to:
As pointed out in my blog post, I'm looking to draw inspiration from redux-sagas for #1. For #2, I think it's important to be able to dispatch an action and await the completion of any resulting asynchronous tasks before moving on to the next line of the test. I want to be able to test my application core with zero chance of a timing-related failure or time wasted polling for some change to the application state. A threading model should probably be pluggable, but redux is a pattern for stateful client apps and nearly every platform has a main thread restriction. A threading model is a blindspot for redux in javascript, because javascript doesn't have threads. I think a good default is for events to be broadcast on the main thread, regardless of where they're dispatched. In most cases state changes will be bound into the UI. Handling Store events on any other thread is, to me, really the special case. IMO, a pattern requiring devs to remember to e.g. Redux.NET need not solve all of these challenges, but hopefully the extension mechanisms you mentioned above could be designed in a way to facilitate these things. |
@cmeeren and @dcolthorp Thanks for your feedback. I'm sure a lot of dot net developers feel the same way you do - a dot net library should feel like dot net. I created my implementation purely to see if it was possible. Because of my background in javascript and existing patterns used in redux it felt familiar to me. That being said I have no problem keeping redux.net feeling more like dot net, and in fact I think it should stay that way if it will be more familiar for other dot net developers to adopt. I do think that if we are going to call it redux then it should have the same features as traditional redux. One such feature is the For combine reducers I think that what @cmeeren showed with the root reducer that just passes the action down the chain is super simple and super clean. As I said before I created mine more as a challenge to see if it would be possible - also to have the same feel as redux. It was a learning experience for me and I think keeping redux.net more dot net like is better. As for the middleware it is going to be tough to have similar functionality as redux. Explicitly casting any time you want to do something with the result from a dispatch is not ideal. I solved part of this by providing a method with the middleware that did the casting so that the developer would not be responsible for doing the casting. Here is an example. public class LiteDbMiddleware {
public static Middleware<TState> CreateMiddleware<TState>(String databaseName) {
var db = new LiteDatabase(databaseName);
var continuum = db.GetCollection<PointInTime<TState>>("spacetime");
return store => {
var pointsInTime = continuum.FindAll().ToList();
pointsInTime.ForEach(point => store.Dispatch(point.LastAction));
return next => action => {
if (action is GetHistoryAction) {
return continuum.FindAll().ToList();
}
var result = next(action);
continuum.Insert(new PointInTime<TState> { LastAction = action, State = store.GetState() });
return result;
};
};
}
public static IEnumerable<PointInTime<TState>> GetHistory<TState>(Dispatcher dispatch) {
return dispatch(new GetHistoryAction()) as IEnumerable<PointInTime<TState>>;
}
} this is a simple middleware that I created that stores every action into a LiteDB (simple bson file) and then reads all the actions back out and applies them to the store when the app starts. Its a super basic event-sourcing example. In the example you can see that i also bundled with it an action Also I would point out that in response to
redux does explicitly prohibit this which can be seen in this unit test, which is why I did it in my port. Like you say it would not really be feasible to do and would be considered bad practice, so explicitly disallowing it makes sense. I'm really glad that there is a lot of discussion going on and I appreciate all the time put into the comments here. I think that redux is awesome and i'm glad to be a part of the community helping bring it to dot net |
It seems to me that we do actually agree on most core issues. Here's a rough spec sheet based on the current feedback:
|
@cmeeren nice work simplifying what we have talked about. I like it. I have a question about your reducer example that you've given. In the root reducer Execute method you take in the previous state and action and return a new root state. It looks like that happens no matter what action is dispatched - which means that the root state object reference will change every time any action is dispatched even if none of the reducers are listening for that action. Do you think that should be the expected behavior? I would imagine that the state should remain the same if an action is dispatched that doesn't match. I haven't tried this out yet but i'm curious if there would be unnecessary listeners or events fired. Thoughts? |
@lilasquared Okay, here's a few thoughts on that:
And finally,
|
Wow, I was not expecting that much feedback! Really appreciated!
I agree. A StateChanged event is a lower entry barrier than an Observable.
I see 3 ways of doing it
public static IObservable<TState> ObserveState(this IStore<TState> store)
{
return Observable.FromEvent<TState>(
h => store.StateChanged += h,
h => store.StateChanged -= h);
}
1 and 2 introduce breaking changes, 3 increase the surface API and force store enhancers to implement the Subscribe method.
I agree. See #53 Thanks a lot @lilasquared ! :)
Store enhancers are definitely a great extension point. However, if we want to lower the entry barrier for C# developers, I'm not sure supporting the store enhancers exactly like redux.js does is a good idea. IMO, even if C# has more and more functional features, a store enhancer could be replaced by an OO decorator. What do you think about it?
I completely agree.
Broadcast events on the UI thread will be difficult to integrate inside Redux because it's a platform specific task. I think it can be done via platform specific store enhancer but it forces to maintain multiple nugets. (Like Rx for scheduling).
@dcolthorp I suppose the need to "Support awaiting completion of async stuff in integration tests" is a consequence of broadcasting on the UI thread. If the store enhancer expose a DispatchAndPublishAsync method, you would be able to write tests like in your article right? Regarding the reducer structure, I agree with @cmeeren. It worked well for me when I was working on an app with a large state. @dcolthorp I didn't know about Lenses. Really interesting! Thanks! |
@GuillaumeSalles I think you have touched on one of the main points of concern that I have with the way the events / observables are being implemented or proposed to be implemented. You mention that:
And you are 100% correct. Platform specific things should not exist within redux. If you were to use redux with any modern javascript library you would most likely use redux and a redux-helper to integrate with your platform. (see react-redux, ng-redux, backbone-redux you get the idea). Redux was designed to have low-level api that is easy to extend. With that being said there are quite a few more reasons to be able to easily extend in javsacript. I am wondering if those reasons exist in dot net too. I am totally behind leveraging the dot net best practices to create those extensions in a way that is easily consumable. (referring to the decorators comment) However I am pretty sure redux doesn't behave the way it does simply because it is javascript and not C#. I can think of at least two extension points / helper libraries. One for MVC, and one for WPF, Maybe one for univeral apps? I'm not too sure. It may even be possible to create a helper that makes it easier to use with the command line. I like the flexibility that redux has so I guess my question for this project is: Is the intention to create something low level that can be extended, or something that is single use everywhere. The guys who made and maintain redux have put a lot of thought into all of the features and you can read up about why they behave the way they do. One example is the discussion about not returning the state in the subscriber callback. They thought about doing it at one point until this comment:
(source : reduxjs/redux#303) If you continue scrolling you can see exactly why they decided to keep it the way it is because creating extensions becomes harder. So back to the original question which is do we feel like there will be need for extensions and should it be easier to create those by leaving the low level api of redux orthogonal |
Since this is a major upgrade (2.x → 3.0), breaking changes are perfectly fine (see SemVer if you're not familiar with it; all libraries should adhere to semantic versioning IMHO). We should lay the foundation for a solid library going forward, and that will likely include several breaking changes.
No strong feelings on my part, but that's mostly because I lack the experience/knowledge to fully grasp the implications (I'm not that familiar with functional concepts, and while I know of the decorator pattern, I haven't used it myself yet). Whichever solution we arrive at should be robust, extensible, and simple to use.
Interesting, thanks. I'm leaning towards making the "raw" store low-level/orthogonal and having separate higher-level extensions (whether as functional store enhancers or OO decorators) to e.g. serve the current (and possibly previous) state to subscribers. I would prefer the most important of these extensions to be included with Redux.NET according to what @dcolthorp said about a a batteries-included, prepackaged "starter kit". Regarding the UI thread stuff: I haven't done anything such with Yarni, yet everything works perfectly. Under which circumstances would this be a problem? (In my app, all subscribers are defined in the view codebehind and update the view directly; I don't use bindings. Perhaps that's why things work for me?) Regarding the "awaiting completion" stuff: What about middleware? Middleware may dispatch actions forward (ultimately reaching the store) and then do other stuff that the store can't possibly know about. See e.g. my ListenerMiddleware which sends the action down and then calls all listeners (subscribers), which will perform necessary async tasks and often dispatch actions of their own. So again, I'm not sure what exactly @dcolthorp intends to be awaited. |
I took a closer look at @dcolthorp's blog post now, and I must say I find the projection philosophy quite compelling, where We might consider implementing and encouraging this design, because reorganizing the app state (as is necessary as the app grows) is a pain if you're using the state directly (I spent an hour today doing that, most of it fixing unit tests, and the state was fairly small). @dcolthorp, you provide the following example in your blog post: App.Store.Observe(Projections.Todos.FilteredTodos)
.Subscribe(visibleTodos => TodosItemsControl.ItemsSource = visibleTodos); What if there's several UI components you want to update? I immediately see at least two ways of doing it: Alternative 1 – calling App.Store.Observe(Projections.Todos.FilteredTodos)
.Subscribe(visibleTodos => TodosItemsControl.ItemsSource = visibleTodos);
App.Store.Observe(Projections.Todos.FilterState)
.Subscribe(filter => ShowAllControl.Checked = filter == TodoFilter.ShowAll);
App.Store.Observe(Projections.Todos.AllTodos)
.Subscribe(allTodos => EmptyMessage.IsVisible = !allTodos.Any()); Alternative 2 – having a projection that returns something like a UI-specific state: App.Store.Observe(Projections.TodoListState)
.Subscribe(state => {
TodosItemsControl.ItemsSource = state.visibleTodos;
ShowAllControl.Checked = state.filterState == TodoFilter.ShowAll;
EmptyMessage.IsVisible = !state.allTodos.Any();
}); |
Hi everyone! A few points of followup. ProjectionsI'm glad you like the projections idea @cmeeren! We've been doing one observe per property, but with a couple of twists on your example above: First, we're using a MVVM-first approach that allows us to bind observables to a property. So in our view model we'd have something like Second, one of the reasons I like the term "projection" rather than "selector", is that selector implies substructure extraction, whereas I try to define projections that are high level and semantic in intent. So as you can see in my ReactiveProperty example in the proceeding paragraph, I'd simply define a high level projection that lets me ask the semantic question "Do I have any todos", rather than calling Rx really becomes helpful when combining these two approaches. Sometimes you simply need to invert a boolean or run a projection's domain-level result through a presentation function. Linq really solves this problem well, allowing you to do things such as MyProperty = ReactiveProperty.Create(store.Observer(Projections.SomeDomainProjection)
.Select(myPresentationFunction)) Linq/Rx therefore provides the high level glue we can use to bring together domain projections and view-level concerns in a declarative way. ThreadingTotally agree that the threading shouldn't be fixed within redux, but important to support the model. I've parameterized my implementation with a Asynchronous actionsI hope to get heads down on this pretty soon, and maybe blog about where I end up. For asynchronous actions, my thinking was to be able to define functions of type These asynchronous handlers could do e.g. HTTP requests, and would then dispatch actions back into the store to reflect progress, or even trigger subsequent async actions. (Redux sagas introduces some additional terminology I'm leaving out here.) These sagas can run in any thread, as dispatching to the store is safe from any thread. Observers are still notified on the main thread, so any state changes that happen during the process can be safely bound to the UI regardless of where they were dispatched. Crucially, awaiting the I think this structure would give you a clean, well-factored way to deal with asynchrony on the one hand, but also solve a long-standing issue with system tests – namely, slowness due to e.g. UI polling and inconsistent results due to timing issues. A system constructed in this way would track the precise information about when domain model changes have occurred, allowing end-to-end system testing of the application core in a high level way. That's the hope, at least. |
@dcolthorp, could you share the implementation of I actually just have the basic signature right, I think: internal static class StoreExtensions
{
public static IObservable<TProjection> Observe<TState, TProjection>(
this Store<TState> store,
Func<TState, TProjection> projection)
{
// create and return an Observable<TProjection>
}
} |
On another note, I'm kind of thinking maybe we should just have (on the "base" store or an enhancer we ship) a |
If I understand the projection concept correctly it should be : With redux.net 2.0.0 public static IObservable<TProjection> Observe<TState, TProjection>(
this Store<TState> store,
Func<TState, TProjection> projection)
{
return store.Select(projection);
} With an event based store public static IObservable<TProjection> Observe<TState, TProjection>(
this Store<TState> store,
Func<TState, TProjection> projection)
{
return Observable.FromEvent<TState>(
h => store.StateChanged += h,
h => store.StateChanged -= h)
.Select(projection);
} I fail to see the difference between @dcolthorp Observe and the IObservable.Select method. If I understand correctly, the difference is the context in which is used. Observe for domain concern and Select for view concern. |
Thanks! If it's that short and concise, @dcolthorp, what's the point of having an Observe method and calling |
Ah, sorry. Observe is just what we called Select on our Store implementation (not redux.net). The main goal is to not expose the core We also have a |
For what it's worth, here's a current snapshot of our The key thing to note is that Also note that all of the RefFunc etc. stuff is a consequence of the choice to use structs for organizing the store. Switching to a fully immutable State type would allow us to do everything with normal lambdas. This code is very much a snapshot of a work in progress and not released anywhere, just extracted from the xamarin app I'm working on. It's a playground for ideas, and extensibility etc. is currently not a goal. 😀 |
Thanks, that clears it up. I'm leaning towards exposing the state (so that we don't force added layers/complexity, e.g. projections, on people who have simple needs). However, we might consider hiding it in an enhancer or subclass or similar, which instead surfaces Select (whatever the name) and Project. Thoughts? |
I have no problem with that. :-) My aim is figuring out the API I want to program to, not designing an API everyone else should use. I'm happy to wrap a lower-level implementation (like I did with P.S. One difference between |
I agree that this would be the expected behavior. |
IMHO, the truth lies in between. Redux.js API is reduced to the bare minimum thanks to the ton of feedback from the community. As @lilasquared noted, there is some differences between the C# version and the js version.
However, for the Redux.js Subscribe method vs C# events, I think there is no semantic differences.
I know semver and I want to follow it with Redux.NET. But if the breaking change is to huge and does not offer a clear upgrade path, people tend to be angry... (E.g angular 1.4 -> angular 2.0). But it's probably bikeshedding because Redux.NET is clearly not as popular as Angular :D ! I don't think so much people are using redux.NET (but I don't really know...) so I'm open to break things heavily. Upgrade documentation should be enough.
I think the solution 2 is concise and robust.
It happen when dispatch is called from a background thread. It does not appear often because the dispatch is often called directly after a user input or after awaiting a network call (so we are still on the UI thread if ConfigureAwait(false) has not been used). I had to dispatch from the background thread for performance optimisation. In theory, actions creator, middleware, reducer, selectors (= projection) could be executed on the background thread and only use the UI thread when the view is updated.
It's currently hard to dispatch asynchronously from inside a middleware with Redux.NET because the Dispatch signature. (object -> object or IAction -> IAction). The Dispatch caller need to cast the dispatch return to something awaitable. If I understand correctly your listeners implementation, you need to use an async void delegate to execute async calls? It can be solved if we modify the Dispatch delegate with object -> Task but I don't think it should be done in the core library. Maybe an AsyncStore enhancer? |
@dcolthorp Redux-saga is extremely interesting but I think we can all agree that it should be built as an extension. I reopen the issue #47 to go deeper on this topic. It would be impressive to build Saga in C#. |
We could implement this as an enhancer, or make it optional through the store constructor or some CreateStore method or whatever we end up with. |
Currently reading up on Rx and liking what I learn. I came across the following on this page:
Furthermore:
See the page for further details. Currently, the |
I mentioned somewhere I'd post my (trivial) implementation of ProjectingStore. Here it is: namespace Redux
{
using System;
using System.Reactive.Linq;
using JetBrains.Annotations;
/// <summary>
/// A wrapper for <see cref="C:Store{TState}" /> whose state
/// is only accessible through projections.
/// </summary>
public class ProjectingStore<TState>
{
private readonly Store<TState> store;
/// <inheritdoc cref="Store{TState}" />
public ProjectingStore(
Reducer<TState> reducer,
TState initialState,
params Middleware<TState>[] middlewares)
{
this.store = new Store<TState>(reducer, initialState, middlewares);
}
/// <inheritdoc cref="M:Store{TState}.Dispatch" />
public IAction Dispatch(IAction action)
{
return this.store.Dispatch(action);
}
/// <summary>
/// Returns an observable of a projection of the state. Uses DistinctUntilChanged
/// to only push elements when the projection result changes.
/// </summary>
public IObservable<TProjection> Observe<TProjection>(
Func<TState, TProjection> projection)
{
return this.store.Select(projection).DistinctUntilChanged();
}
/// <summary>Returns a (snapshot) projection of the current state.</summary>
[CanBeNull]
public TProjection Project<TProjection>(
Func<TState, TProjection> projection)
{
return projection(this.store.GetState());
}
}
} |
@GuillaumeSalles There has been little activity in this repo for a while now. I'm specifically thinking of the outstanding pull requests (#61, #62, #64). Everything okay? Do you need anything more? |
@GuillaumeSalles Is the Redux.Net project still active? |
I also rolled my own for a work project. I also used an Rx Extensions focused design, with some built-in functionality inspired by the redux-observable project in JS land. It didn't have support for middleware though, which would have been better than my built-in redux-observable design, but it was just a small library to get going. I also wrote some helper methods to make binding UI to Redux state a little easier. I too noticed how difficult it can be to mix MVVM and Redux styles in a single project. If possible, putting all state in Redux and keeping view bindings to state as simple as possible is the only sustainable way to handle it for MVVM heavy frameworks like UWP, WPF, and XF. However, the model works a lot better with Blazor I've noticed, which is more like React and lends itself better to one-way bindings to observable state. I write all the application Redux code (state, actions, and reducers) in F# and reference the F# library from the view project, which is C#. Not ideal but C# is too verbose to implement Redux properly. Perhaps C# 9 will help? |
I open this issue to know what kind of features people would want in a v3.
I don't currently use Redux.NET in my day to day job so I can't really build something useful without feedback.
What improvements I currently have in mind :
cc @cmeeren, @lilasquared, @dcolthorp :
You definitely put a lot of time to think about Redux or even use it in production. I think we can benefit from our different experiences to shape a new version of Redux that we will help everyone. Your feedback is more than welcome and I'm looking for some people that want to become core contributors of Redux.NET since I don't always have the time to work on it anymore.
The text was updated successfully, but these errors were encountered: