-
Notifications
You must be signed in to change notification settings - Fork 58
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
Add BuiltReduxUiComponent #118
Add BuiltReduxUiComponent #118
Conversation
RavenNumber of Findings: 0 |
pubspec.yaml
Outdated
@@ -9,6 +9,8 @@ environment: | |||
dependencies: | |||
analyzer: ">=0.30.0 <0.31.0" | |||
barback: "^0.15.0" | |||
built_redux: ^6.1.0 | |||
built_value: ^4.2.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this dependency needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I guess it's only needed for tests, will move
pubspec.yaml
Outdated
@@ -9,6 +9,8 @@ environment: | |||
dependencies: | |||
analyzer: ">=0.30.0 <0.31.0" | |||
barback: "^0.15.0" | |||
built_redux: ^6.1.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This dependency worries me since it's undergoing major version bumps quite frequently. If we include this, that means we have to stay on top of bumping the upper bound to avoid being a source of version lock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's a good point
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It only looks that way because, per @davidmarne-wf, because he went 1.0 too early and broke some things so he had to rev for semver purposes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I understand your hesitation for sure, but I really do believe it finally settled down. I'm very happy with where it stands right now.
Plus at this point it is a dependency for CDS, w_comments, review_bar, and soon to be graph_app (and many other repos via transitivity from w_comments) so any major bumps are already going to have to be dealt with anyways.
However, if we want I could easy make another repo that just has this component.
Codecov Report
@@ Coverage Diff @@
## master #118 +/- ##
==========================================
+ Coverage 94.42% 94.44% +0.02%
==========================================
Files 31 32 +1
Lines 1559 1581 +22
==========================================
+ Hits 1472 1493 +21
- Misses 87 88 +1 |
Is this set of base classes a pattern that's also being used in comments? Trying to get a feel for how mature this approach is, and whether more exploration is needed before committing to this API. |
I believe w_comments it just listening to the streams on mount. Let me double check |
And to clarify, I'm not against this approach, I just want to make sure it's tried and true before adding it to this repo. |
@greglittlefield-wf You can see their usage here |
@@ -0,0 +1,163 @@ | |||
// Copyright 2016 Workiva Inc. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/2016/2017
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same goes for all files added in this PR
abstract class ReduxUiProps<StoreT> extends UiProps { | ||
String get _storePropKey => '${propKeyNamespace}store'; | ||
|
||
/// The prop defined by [StoresT]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/StoresT/StoreT
|
||
/// The prop defined by [StoresT]. | ||
/// | ||
/// This object should either be an instance of [Store]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
either an instance of [Store] ... or ... what else? ;)
@@ -0,0 +1,222 @@ | |||
// GENERATED CODE - DO NOT MODIFY BY HAND |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jacehensley-wf is it necessary to commit generated files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't commit, build it into your build process to build before tests run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I can just build it during CI, but while deving you'll have analysis warnings till you generate
tool/build.dart
Outdated
import 'package:source_gen/source_gen.dart'; | ||
import 'package:built_redux/generator.dart'; | ||
|
||
/// Build the generated files in the built_value chat example. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What chat example?
|
||
@override | ||
void setState(_, [callback()]) { | ||
print('here'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Unbind store subs so they can be re-bound in componentDidUpdate | ||
// once the new props are available, ensuring the values returned by [redrawOn] | ||
// are not outdated. | ||
_tearDownSubs(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the store prop is the same, we don't need new subs, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not worth checking that, it doesn't take much to teardown and re-bind the subs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious why it isn't worth it. It seems like you'd want to not force something that is not necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tearing down and rebinding subscriptions is cheap, I don't think it's necessary to expend effort to avoid it.
Also, we we'd have to compare the return value of redrawOn
and not just props.store
, and we don't have access to that updated return value until componentDidUpdate
(since that function can rely on values within props
). So, we'd have to store the old values in here and then check them and null them out in componentDidUpdate.
In this case, unbinding/rebinding is more straightforward, and again, should cheap enough to be worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused about this, tracking _areStoreSubsBound and having all these teardown and setups it seems overly complicated to me.
The reference to your store should never change, it seems to me that you would only setup subs on componentWillMount and remove them on componentWillUnmount.
@greglittlefield-wf, the preferred use in Redux.js is to use a HOC called a provider and connect the data primitive components. In Dart, we don't have that until react-dart supports context. We're starting to use this in grc and we're looking at a wrapper component vs a base component. The typical flow, per David, is to have a wrapper component handle the subscription and wire up the child components from there. This PR would be used as base class(es) we extend for our wrapper component. |
Oh and you really should bring over the pure component in Comments. If we're using built-redux, a |
@johnbland-wf We can't do the HOC until we add support for React's context to react-dart. For the |
} | ||
} | ||
|
||
abstract class _ReduxComponentMixin<T extends ReduxUiProps> implements UiComponent<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make a lot of sense to have a connect function here. The default implementation could be something like
Substate connect(State state) => state;
But then that could be overridden to only redraw when specific pieces of your state change
@override
Substate connect(State state) => state.myComponentWantsToRedrawOn;
then redrawOn would be something like
List<Stream> redrawOn() {
if (props.store is Store) {
return <Stream>[props.store.nextSubstate(connect)];
} else {
return <Stream>[];
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With that how would someone listen to multiple stores?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no concept of multiple stores. It is one store in redux.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, what about multiple values on the single store?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. We'll want a substate on multiple values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So one thing you can do is map all the values into a new built_value. For example say by store's state object is:
abstract class AppState implements Built<AppState, AppStateBuilder> {
int get foo;
int get bar;
int get baz;
}
But my component only wants foo and bar. I can still only using a single nextSubstate by defining a new model that contains the subset of state my component cares about and having my connect function return that. So i would define
abstract class DataComponentWants implements Built<DataComponentWants, DataComponentWantsBuilder> {
int get foo;
int get bar;
}
and have connect do:
DataComponentWants connect(AppState state) => new DataComponentWants(
foo: state.foo,
bar: state.bar,
);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since DataComponentWants is also a built, it should be comparable with the last result of connect, so nextSubstate will only trigger if the resulting value of DataComponentWants changes after the AppState change.
@jacehensley-wf yes (re: react context). I mentioned that in hopes @greglittlefield-wf would find it in his heart to push that forward. ;) Agreed on Note: It really is a |
My main concern was marching forward with this without thoroughly planning this feature, but it seems like there's a consensus that this is what we want 👍. |
/// * Redux components are responsible for rendering application views and turning | ||
/// user interactions and events into [Action]s. | ||
/// * Redux components can use data from one or many [Store] instances to define | ||
/// the resulting component. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just one store, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup
I threw up a proposal on how I could see this working here: The generics get a bit verbose, but it is necessary to get legit type safety. |
@davidmarne-wf How does this look? |
_storeSub = props.store.nextSubstate(connect).listen((Substate s) { | ||
_connectedProps = s; | ||
redraw(); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the only problem with this is we can only listen to one substate so we'd be stuck if we wanted state.count
and state.todos
but not state.options
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts, @davidmarne-wf?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, your substate would be a built_value object like shown in the doc comment https://github.com/Workiva/over_react/pull/118/files/20dc99def5af7766e5045dbcc909ad3bb39db8f1#diff-cd6ab537d255abbf1ec247df0369120eR119
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i am of the opinion you define a new built_value and map state to that. so say your state is
abstract class State extends Built<State, StateBuilder> {
int get count;
BuiltList<Todo> todos;
Map<String, String> options;
...
}
you define a model
abstract class SubstateMyComponentWants extends Built<SubstateMyComponentWants, SubstateMyComponentWantsBuilder> {
int get count;
BuiltList<Todo> todos;
...
}
then your connect is
@override
SubstateMyComponentWants connect(State s) => new SubstateMyComponentWants((builder) => builder
..count = state.count
..todos = state.todos);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'ld be open to suggestions for multiple connectors though. I imagine it would look something like:
typedef Substate StateMapper<State, Substate>(State s);
Iterable<StateMapper<State, dynamic>> get connectors;
...
void _setUpSub() {
_storeSubs.addAll(connectors.map((connect) => props.store.nextSubstate(connect).listen((_) {
redraw();
}));
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then you wouldn't have connectedState, you would prob just expose a store.state getter and let the renderers pull data from that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or you could just have
bool shouldUpdateOnReduxStateChange(State prev, State next));
that the consumer implements.
then if i wanna update on todos or count changes i do:
@override
bool shouldUpdateOnReduxStateChange(State prev, State next) => prev.todos != next.todos || prev.count != next.count
The upside to keeping how it is now, where you load everything into a new built_value, is this comparison is generated for the model.
The shouldUpdateOnReduxStateChange pattern would be less code when you are only pulling a couple things from the store, but the connect pattern would be less if you are mapping a lot of data from your store. Plus you are more likely to goof up shouldUpdateOnReduxStateChange and forget to check a param you want to update on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the first option, with using built_value objects to contain multiple state values
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fine.
/// int get baz; | ||
/// } | ||
/// | ||
/// abstract class DataCoponentCaresAbout implements Built<DataCoponentCaresAbout, DataCoponentCaresAboutBuilder> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DataCoponent
-> DataComponent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments on test coverage and some other small things, otherwise looks really close!
Also, PR description needs updating (e.g., ReduxUiStatefulComponent
is no longer included).
set store(Store<V, B, A> value) => props[_storePropKey] = value; | ||
|
||
/// The [ReduxActions] prop defined by [A]. | ||
A get actions => store.actions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add test coverage for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@override | ||
void componentWillReceiveProps(Map nextProps) { | ||
super.componentWillReceiveProps(nextProps); | ||
_tearDownSub(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this method is missing test coverage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@mustCallSuper | ||
@override | ||
void componentWillUpdate(Map nextProps, Map nextState) { | ||
if (_storeSub == null) _setUpSub(nextProps); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this method is missing test coverage
Substate> implements UiComponent<T> { | ||
Substate _connectedState; | ||
|
||
/// The substate values of the redux store that this component component subscribes to. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: component component
|
||
/// The substate values of the redux store that this component component subscribes to. | ||
/// | ||
/// It is reccommened to use this instead of `props.store.state`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#nit comma should be a semicolon
/// Related: [connect] | ||
Substate get connectedState => _connectedState; | ||
|
||
/// Subscribe to changes to the values from the redux store that this component cares about. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#nit This doesn't follow the Dart style guide convention around the first line of function doc comments.
Should be something like:
Returns a subset of the values derived from the redux store that this component cares about.
@mustCallSuper | ||
@override | ||
void componentWillUpdate(Map nextProps, Map nextState) { | ||
if (_storeSub == null) _setUpSub(nextProps); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to add a code comment here that explains when this is null (i.e., for component props updates and not just any rerender)
TestDefaultComponent component = getDartComponent(renderedInstance); | ||
|
||
store.actions.trigger1(); | ||
await new Future.delayed(Duration.ZERO); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary now that actions dispatch synchronously? Should we set the lower bound to the major version that changes that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to leave it open to both version to not be blocking, I don't think w_comments is on that major version yet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
If you end up making changes in this PR again, perhaps a todo comment would be good to remove these if we ever bump. Waiting extra long shouldn't hurt, but it would be nice to verify that the things we expect to be synchronous are indeed synchronous.
reason: 'component should no longer be listening after unmount'); | ||
}); | ||
|
||
test('subscribes to any state changes subscribed to in connect', () async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This description would be clearer and more indicative of what's being tested if it read "only the state changes subscribed to in connect".
expect(component.numberOfRedraws, 1); | ||
|
||
component.props = TestDefault()..store = updatedStore; | ||
component.redraw(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be actually rerendering the component and not just simulating a props update.
One more attempt for a rename here. ;)
I highly suggest renaming this so it fits the naming of Built*. |
+1 |
…upport/dev # Conflicts: # pubspec.yaml
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
+1 refresh |
+1 |
QA +1
Merging into master. |
Ultimate problem:
We should have support for the built_redux library.
How it was fixed:
BuiltReduxUiProps
andBuiltReduxUiComponent
that wire up listeners to the store's stream.Testing suggestions:
Potential areas of regression: