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

Context support #135

Merged
merged 32 commits into from
Dec 14, 2017
Merged

Context support #135

merged 32 commits into from
Dec 14, 2017

Conversation

johnbland-wf
Copy link
Contributor

Feature

Support context.

Solution

  • Provided support for context without a breaking change by adding extra *WithContext methods. All of these methods replace the normal lifecycle methods so both are not needed.
    • componentDidUpdateWithContext
    • componentWillUpdateWithContext
    • shouldComponentUpdateWithContext
    • componentWillReceivePropsWithContext
  • shouldComponentUpdateWithContext returns null by default so shouldComponentUpdate is still used. If shouldComponentUpdateWithContext returns a valid bool, shouldComponentUpdate will not get checked
  • Updated tests to verify all of the lifecycle methods are called

Testing

  • Verify unit tests pass
  • Verify context example makes sense and works as expected

config.childContextTypes[childContextKeys[i]] = React.PropTypes.object;
}

// Only declare this when `hasChildContext` is true to avoid unnecessarily

Choose a reason for hiding this comment

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

Referencing an old variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly. I'll dig into it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think so, sorry about that. Should probably read when childContextKeys is non-empty

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@greglittlefield-wf
Copy link
Collaborator

@greglittlefield-wf @aaronlademann-wf @jacehensley-wf @clairesarsam-wf

Copy link
Collaborator

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

Looks good, mainly just an issue around context updating and related test coverage.

So sorry it took me so long to take a look at this; I'll definitely be quicker to respond moving forward.

lib/react.dart Outdated
_initProps(props);
}

/// Initializes context
_initContext(context) {
this.context = new Map.from(context ?? {});
Copy link
Collaborator

Choose a reason for hiding this comment

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

#nit To reduce garbage, this could be const {}.

lib/react.dart Outdated
_initContext(context) {
this.context = new Map.from(context ?? {});

/// Call `transferComponentContext` to get state also to `_prevContext`
Copy link
Collaborator

Choose a reason for hiding this comment

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

#nit This should not be a doc comment

lib/react.dart Outdated
_initProps(props) {
this.props = new Map.from(props);
this.nextProps = this.props;
}

initStateInternal() {
this.state = new Map.from(getInitialState());
// Call `transferComponentState` to get state also to `_prevState`

/// Call `transferComponentState` to get state also to `_prevState`
Copy link
Collaborator

Choose a reason for hiding this comment

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

#nit This should not be a doc comment

lib/react.dart Outdated
@@ -114,16 +146,31 @@ abstract class Component {

/// Public getter for [_nextState].
Copy link
Collaborator

Choose a reason for hiding this comment

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

s/_nextState/_nextContext/

component.props = component.nextProps; // [1]
component.transferComponentState(); // [2]
component.transferComponentContext(); // [3]
Copy link
Collaborator

Choose a reason for hiding this comment

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

From this code, and from testing the example, context doesn't seem to ever get updated.

nextState gets updated in setState and then transferred to state in transferComponentContext, but there doesn't seem to be an analagous updating of nextContext here.

I would recommend updating how context is managed to mirror props as opposed to state, since they both are updated externally.

This updating should also have test coverage (see later comment in tests).

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 believe that is a react-js issue: facebook/react#2517.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe that issue is around how to handle context not getting updated when shouldComponentUpdate returns false.

Otherwise, context should update.

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 agree. I'm validating that. The initial code had this issue and I found several references of it not updating.

I'll ping back once I confirm it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Built a react-js app and it does indeed update. Investigating it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok...it is refactored to match the props integration.

lib/react.dart Outdated
/// provides no added benefit.
///
/// See: <https://facebook.github.io/react/docs/react-component.html#updating-componentdidupdate>
void componentDidUpdateWithContext(Map prevProps, Map prevState, Map prevContext) {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since React 16 doesn't include prevContext in componentDidUpdate, can we just omit this lifecycle method?

]));
});

group('when shouldComponentUpdate returns false:', () {
void testShouldUpdates({bool shouldComponentUpdateWithContext, bool shouldComponentUpdate}) {
test('recieves updated props with correct lifecycle calls and does not rerender', () {
Copy link
Collaborator

Choose a reason for hiding this comment

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

There should be tests that context maps are updated as expected and with the correct values in the correct lifecycle methods.

An easy way to do that would be to parallel this props update test as well as the other "recieves updated props" test.

@johnbland-wf
Copy link
Contributor Author

Ok, this now updates context as expected. It isn't yet unit tested, but the big hurdle is done. Gotta jet. I'll tighten this up tomorrow.

@johnbland-wf
Copy link
Contributor Author

Ok, @greglittlefield-wf...final review time.

That test found some issues w/ the wrong context being passed to lifecycle methods. Let me know how it sits.

lib/react.dart Outdated
@@ -139,6 +172,8 @@ abstract class Component {
///
/// [A.k.a "forceUpdate"](https://facebook.github.io/react/docs/react-component.html#forceupdate)
void redraw([callback()]) {
context = getChildContext();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this happen here but not in setState?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hrmm...guess I mistook JS for Dart here w/ the _jsRedraw vs redraw.

Should it go in replaceState as well?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, what's the purpose of doing this anyways? Doesn't this https://github.com/cleandart/react-dart/pull/135/files#diff-12ac054bb8b76f35c1035afef1d94396R257 update the context?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great find. I removed it and all tests pass + examples work. See 723caa5.

@jacehensley-wf
Copy link
Contributor

+1

Copy link
Collaborator

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

One comment on internal context management, some small stuff, and a couple areas where test coverage could be improved.

Otherwise, looks great!! This is really close!

lib/react.dart Outdated
/// Public getter for [_nextContext].
///
/// If `null`, then [_nextContext] is equal to [context] - which is the value that will be returned.
// Map get nextContext => _nextContext == null ? context : _nextContext;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Dangling doc comment. Looks like this should be the doc comment for nextContext whose current comment doesn't seem to match.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, that was dead code from a previous build. Removing...

void _afterPropsChange(Component component) {
component.props = component.nextProps; // [1]
component.transferComponentState(); // [2]
/// 2. Update [Component.context] using the value stored to [Component.nextContext]
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment doesn't patch up with what's being done.

Also, in handleComponentWillReceiveProps, why isn't nextContext being stored onto the component like props is? It's likely better for perf to match that pattern and only call _unjsifyContext once per rerender vs the 3 times per rerender that it's called currently.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, man...too many twists and turns in here. :-D Great find. I found where the issue was w/ nextContext being outdated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See f0d8b3a.

@@ -178,8 +192,7 @@ class InteropProps {
external factory InteropProps({ReactDartComponentInternal internal, String key, dynamic ref});
}

/// Internal react-dart information used to proxy React JS lifecycle to Dart
/// [Component] instances.
/// A Dart object that stores .
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like this was leftover from my one of my commits, sorry 😬. Can you revert this doc comment change, please?

@@ -27,15 +27,28 @@ var ReactSetStateTestComponent = React.createClass({
this.recordLifecyleCall("componentWillReceiveProps");
},

componentWillReceivePropsWithContext: function(_) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

These -WithContext methods on the JS component won't ever get called, right? Why do they exist?

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 believe it was only testing so recordLifecyleCall would record the call and could be validated.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure I'm following. recordLifecycleCall won't be called since nothing is calling these methods on the JS component, right?

});

group('when shouldComponentUpdateWithContext returns false:', () {
testShouldUpdates(shouldComponentUpdateWithContext: false, shouldComponentUpdate: false);
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we add one more test case that shouldComponentUpdate is not called when shouldComponentUpdateWithContext returns true? Or is that overkill?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's pretty much what this is testing:

        List calls = [
          matchCall('componentWillReceiveProps', args: [newPropsWithDefaults],                props: initialPropsWithDefaults),
          matchCall('componentWillReceivePropsWithContext', args: [newPropsWithDefaults, expectedContext],                props: initialPropsWithDefaults),
          matchCall('shouldComponentUpdateWithContext',     args: [newPropsWithDefaults, expectedState, expectedContext], props: initialPropsWithDefaults),
        ];

        if (shouldComponentUpdateWithContext == null) {
          calls.add(
            matchCall('shouldComponentUpdate', args: [newPropsWithDefaults, expectedState], props: initialPropsWithDefaults),
          );
        }

When shouldComponentUpdateWithContext is null we validate the method is called. If it was called when shouldComponentUpdateWithContext had a value, this test would fail.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, I just meant testing true as a value in addition to false.

@@ -139,7 +140,78 @@ void main() {
]));
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

There should be a lifecycle tests that asserts that getChildContext is not called when childContextKeys is empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tests incoming. Good call here.

Copy link
Contributor Author

@johnbland-wf johnbland-wf Dec 5, 2017

Choose a reason for hiding this comment

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

See 282914b.

matchCall('render', props: newPropsWithDefaults, context: expectedContext),
matchCall('componentDidUpdate', args: [initialPropsWithDefaults, expectedState], props: newPropsWithDefaults, context: expectedContext),
]));
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

Awesome test! Could we add one more that verifies that only the keys present in childContextKeys are passed down, though?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting an extra key to pass but not having the value in the child should be enough. That's incoming.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See 282914b.

@johnbland-wf
Copy link
Contributor Author

Ok, @greglittlefield-wf, I think it is ready.

Big props to you and @jacehensley-wf for catching those issues.

@greglittlefield-wf
Copy link
Collaborator

+1 looks great!

Going to pull this down and test, and also pull into some other repos for testing.

@greglittlefield-wf
Copy link
Collaborator

  • Context works as expected in example
  • All tests pass in content-shell, dart2js, and DDC
  • All tests pass when pulled into OverReact and WSD

+10

@jacehensley-wf
Copy link
Contributor

+1 refresh

@greglittlefield-wf greglittlefield-wf merged commit 84530cd into Workiva:master Dec 14, 2017
@johnbland-wf johnbland-wf deleted the context branch December 14, 2017 19:50
aaronlademann-wf added a commit to aaronlademann-wf/react-dart that referenced this pull request Mar 6, 2019
+ Deprecates everything that was added in Workiva#135
+ New context API will be released alongside support for ReactJS 16 in version 5.0.0 of this package
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants