Skip to content
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

Merged
merged 18 commits into from Dec 6, 2017

Conversation

jacehensley-wf
Copy link
Contributor

@jacehensley-wf jacehensley-wf commented Oct 3, 2017

Ultimate problem:

We should have support for the built_redux library.

How it was fixed:

  • Add BuiltReduxUiProps and BuiltReduxUiComponent that wire up listeners to the store's stream.

Testing suggestions:

  • Verify tests pass

Potential areas of regression:

  • N/A All new stuff

FYA: @greglittlefield-wf @aaronlademann-wf @clairesarsam-wf @joelleibow-wf
FYI: @davidmarne-wf

@aviary2-wf
Copy link

aviary2-wf commented Oct 3, 2017

Raven

Number 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this dependency needed?

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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

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.

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-io
Copy link

codecov-io commented Oct 3, 2017

Codecov Report

Merging #118 into master will increase coverage by 0.02%.
The diff coverage is 95.46%.

@@            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

@greglittlefield-wf
Copy link
Contributor

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.

@jacehensley-wf
Copy link
Contributor Author

I believe w_comments it just listening to the streams on mount. Let me double check

@greglittlefield-wf
Copy link
Contributor

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.

@jacehensley-wf
Copy link
Contributor Author

@greglittlefield-wf You can see their usage here

@@ -0,0 +1,163 @@
// Copyright 2016 Workiva Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/2016/2017

Copy link
Contributor

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].
Copy link
Contributor

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].
Copy link
Contributor

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
Copy link
Contributor

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?

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log

// 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();

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?

Copy link
Contributor Author

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

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.

Copy link
Contributor

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.

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.

@johnbland-wf
Copy link

@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.

@johnbland-wf
Copy link

Oh and you really should bring over the pure component in Comments. If we're using built-redux, a PureReduxComponent should exist for everyone.

@jacehensley-wf
Copy link
Contributor Author

@johnbland-wf We can't do the HOC until we add support for React's context to react-dart.

For the PureReduxComponent I could see that being useful but I'd rather just get this in for now, and adding something like that later.

}
}

abstract class _ReduxComponentMixin<T extends ReduxUiProps> implements UiComponent<T> {

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>[];
     }
  }

Copy link
Contributor Author

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?

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.

Copy link
Contributor Author

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?

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.

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,
);

Copy link

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.

@johnbland-wf
Copy link

johnbland-wf commented Oct 4, 2017

@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 PureReduxComponent being separate. Just getting the list out there. ;)

Note: It really is a PureImmutableComponent since it would work fine without redux so long as the props respond properly to == (ie - immutable or primitives).

@greglittlefield-wf
Copy link
Contributor

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one store, right?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup

@davidmarne-wf
Copy link

davidmarne-wf commented Oct 9, 2017

I threw up a proposal on how I could see this working here:
https://github.com/davidmarne-wf/over_react/ pull number 1

The generics get a bit verbose, but it is necessary to get legit type safety.

@jacehensley-wf
Copy link
Contributor Author

@davidmarne-wf How does this look?

_storeSub = props.store.nextSubstate(connect).listen((Substate s) {
_connectedProps = s;
redraw();
});
Copy link

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts, @davidmarne-wf?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

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();
    }));
  }

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.

Copy link

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

Copy link
Contributor Author

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

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> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataCoponent -> DataComponent

Copy link
Contributor

@greglittlefield-wf greglittlefield-wf left a 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;
Copy link
Contributor

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?

Copy link
Contributor Author

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();
Copy link
Contributor

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

Copy link
Contributor Author

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);
Copy link
Contributor

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.
Copy link
Contributor

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`,
Copy link
Contributor

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.
Copy link
Contributor

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);
Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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 {
Copy link
Contributor

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();
Copy link
Contributor

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.

@johnbland-wf
Copy link

johnbland-wf commented Nov 16, 2017

One more attempt for a rename here. ;)

ReduxUiComponent fits a redux.dart implementation. This is a forced BuiltRedux implementation.

I highly suggest renaming this so it fits the naming of Built*.

@greglittlefield-wf
Copy link
Contributor

+1

@brianbolton-wf
Copy link

Copy link
Contributor

@aaronlademann-wf aaronlademann-wf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@jacehensley-wf jacehensley-wf changed the title Add ReduxUiComponent and ReduxUiStatefulComponent Add BuiltReduxUiComponent Nov 29, 2017
@aaronlademann-wf
Copy link
Contributor

+1 refresh

@greglittlefield-wf
Copy link
Contributor

+1

@jayudey-wf
Copy link
Contributor

jayudey-wf commented Dec 6, 2017

QA +1

  • Testing instruction
  • Dev +1's
  • Dev/QA +10 with detail of what was tested
    • passing build with tests exercising new functionality
  • Security review (if required)
  • Unit tests created/updated
  • All unit tests pass
  • Rosie has run and reports properly the release the ticket will be included in
  • Areas of regression noted

Merging into master.

@jayudey-wf jayudey-wf merged commit be116f1 into Workiva:master Dec 6, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet