-
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
UIP-2114: Update FluxUiComponent subscriptions when props are updated #77
Changes from 5 commits
da6e6a7
009701b
c89e8f8
18e8222
197e470
ab7e64a
b49e851
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,9 @@ | |
library over_react.component_declaration.flux_component; | ||
|
||
import 'dart:async'; | ||
import 'package:meta/meta.dart'; | ||
import 'package:w_flux/w_flux.dart'; | ||
|
||
import './annotations.dart' as annotations; | ||
import './transformer_helpers.dart'; | ||
|
||
|
@@ -64,7 +66,30 @@ abstract class FluxUiProps<ActionsT, StoresT> extends UiProps { | |
/// | ||
/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation. | ||
abstract class FluxUiComponent<TProps extends FluxUiProps> extends UiComponent<TProps> | ||
with _FluxComponentMixin<TProps>, BatchedRedraws {} | ||
with _FluxComponentMixin<TProps>, BatchedRedraws { | ||
// Redeclare these lifecycle methods with `mustCallSuper`, since `mustCallSuper` added to methods within | ||
// mixins doesn't work. See https://github.com/dart-lang/sdk/issues/29861 | ||
|
||
@mustCallSuper | ||
@override | ||
// Ignore this warning to work around https://github.com/dart-lang/sdk/issues/29860 | ||
// ignore: must_call_super | ||
void componentWillMount(); | ||
|
||
@mustCallSuper | ||
@override | ||
// Ignore this warning to work around https://github.com/dart-lang/sdk/issues/29860 | ||
// ignore: must_call_super | ||
void componentWillReceiveProps(Map prevProps); | ||
|
||
@mustCallSuper | ||
@override | ||
void componentDidUpdate(Map prevProps, Map prevState); | ||
|
||
@mustCallSuper | ||
@override | ||
void componentWillUnmount(); | ||
} | ||
|
||
/// Builds on top of [UiStatefulComponent], adding `w_flux` integration, much like the [FluxComponent] in w_flux. | ||
/// | ||
|
@@ -76,46 +101,101 @@ abstract class FluxUiComponent<TProps extends FluxUiProps> extends UiComponent<T | |
/// Use with the over_react transformer via the `@Component()` ([annotations.Component]) annotation. | ||
abstract class FluxUiStatefulComponent<TProps extends FluxUiProps, TState extends UiState> | ||
extends UiStatefulComponent<TProps, TState> | ||
with _FluxComponentMixin<TProps>, BatchedRedraws {} | ||
with _FluxComponentMixin<TProps>, BatchedRedraws { | ||
// Redeclare these lifecycle methods with `mustCallSuper`, since `mustCallSuper` added to methods within | ||
// mixins doesn't work. See https://github.com/dart-lang/sdk/issues/29861 | ||
|
||
@mustCallSuper | ||
@override | ||
// Ignore this warning to work around https://github.com/dart-lang/sdk/issues/29860 | ||
// ignore: must_call_super | ||
void componentWillMount(); | ||
|
||
@mustCallSuper | ||
@override | ||
// Ignore this warning to work around https://github.com/dart-lang/sdk/issues/29860 | ||
// ignore: must_call_super | ||
void componentWillReceiveProps(Map prevProps); | ||
|
||
@mustCallSuper | ||
@override | ||
void componentDidUpdate(Map prevProps, Map prevState); | ||
|
||
@mustCallSuper | ||
@override | ||
void componentWillUnmount(); | ||
} | ||
|
||
/// Helper mixin to keep [FluxUiComponent] and [FluxUiStatefulComponent] clean/DRY. | ||
/// | ||
/// Private so it will only get used in this file, since having lifecycle methods in a mixin is risky. | ||
abstract class _FluxComponentMixin<TProps extends FluxUiProps> implements BatchedRedraws { | ||
TProps get props; | ||
|
||
abstract class _FluxComponentMixin<TProps extends FluxUiProps> implements BatchedRedraws, UiComponent<TProps> { | ||
/// List of store subscriptions created when the component mounts. | ||
/// | ||
/// These subscriptions are canceled when the component is unmounted. | ||
List<StreamSubscription> _subscriptions = []; | ||
List<StreamSubscription> _subscriptions; | ||
|
||
bool get _areStoreHandlersBound => _subscriptions != null; | ||
|
||
/// Subscribe to all applicable stores. | ||
/// | ||
/// [Store]s returned by [redrawOn] will have their triggers mapped directly to this components | ||
/// redraw function. | ||
/// | ||
/// [Store]s included in the [getStoreHandlers] result will be listened to and wired up to their | ||
/// respective handlers. | ||
void _bindStoreHandlers() { | ||
if (_areStoreHandlersBound) { | ||
throw new StateError('Store handlers are already bound'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @greglittlefield-wf does this need a unit test? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only case this would be hit is if someone doesn't add a super-call. I wasn't sure if it was worth adding a test around, but I can add one if you'd like. |
||
} | ||
|
||
void componentWillMount() { | ||
/// Subscribe to all applicable stores. | ||
/// | ||
/// [Store]s returned by [redrawOn] will have their triggers mapped directly to this components | ||
/// redraw function. | ||
/// | ||
/// [Store]s included in the [getStoreHandlers] result will be listened to and wired up to their | ||
/// respective handlers. | ||
Map<Store, StoreHandler> handlers = new Map.fromIterable(redrawOn(), | ||
value: (_) => (_) => redraw())..addAll(getStoreHandlers()); | ||
|
||
_subscriptions = <StreamSubscription>[]; | ||
handlers.forEach((store, handler) { | ||
StreamSubscription subscription = store.listen(handler); | ||
_subscriptions.add(subscription); | ||
}); | ||
} | ||
|
||
/// Cancel all store subscriptions. | ||
void _unbindStoreHandlers() { | ||
if (!_areStoreHandlersBound) return; | ||
|
||
for (var subscription in _subscriptions) { | ||
subscription?.cancel(); | ||
} | ||
|
||
_subscriptions = null; | ||
} | ||
|
||
@override | ||
void componentWillMount() { | ||
_bindStoreHandlers(); | ||
} | ||
|
||
@override | ||
void componentWillReceiveProps(Map prevProps) { | ||
// Unbind store handlers so they can be re-bound in componentDidUpdate | ||
// once the new props are available, ensuring the values returned [redrawOn] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. returned by? |
||
// are not outdated. | ||
_unbindStoreHandlers(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be worth only doing this if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'd have to compare the equality of the return value of We can do that, but I figured that unbinding/rebinding was cheap enough that it was worth doing so to avoid that kind of comparison. |
||
} | ||
|
||
@override | ||
void componentDidUpdate(Map prevProps, Map prevState) { | ||
// If the handlers are not bound at this point, then that means they were unbound by | ||
// componentWillReceiveProps, and need to be re-bound now that new props are available. | ||
if (!_areStoreHandlersBound) _bindStoreHandlers(); | ||
} | ||
|
||
@override | ||
void componentWillUnmount() { | ||
// Ensure that unmounted components don't batch render | ||
shouldBatchRedraw = false; | ||
|
||
// Cancel all store subscriptions. | ||
_subscriptions.forEach((StreamSubscription subscription) { | ||
if (subscription != null) { | ||
subscription.cancel(); | ||
} | ||
}); | ||
_unbindStoreHandlers(); | ||
} | ||
|
||
/// Define the list of [Store] instances that this component should listen to. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,8 +8,7 @@ import 'dart:html'; | |
import 'package:test/test.dart'; | ||
import 'package:w_flux/w_flux.dart'; | ||
import 'package:over_react/over_react.dart'; | ||
|
||
import '../../test_util/test_util.dart'; | ||
import 'package:over_react_test/over_react_test.dart'; | ||
|
||
part 'flux_component_test/default.dart'; | ||
part 'flux_component_test/handler_precedence.dart'; | ||
|
@@ -164,6 +163,30 @@ void main() { | |
await nextTick(); | ||
expect(component.numberOfRedraws, equals(0)); | ||
}); | ||
|
||
test('updates the subscriptions registered from redrawOn when rerendered with new props', () async { | ||
var stores = new TestStores(); | ||
var newStores = new TestStores(); | ||
|
||
var testJacket = mount<TestRedrawOnComponent>((TestRedrawOn()..store = stores)()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ |
||
var dartInstance = testJacket.getDartInstance(); | ||
expect(dartInstance.numberOfRedraws, 0); | ||
|
||
stores.store1.trigger(); | ||
await nextTick(); | ||
expect(dartInstance.numberOfRedraws, 1); | ||
|
||
testJacket.rerender((TestRedrawOn()..store = newStores)()); | ||
dartInstance.numberOfRedraws = 0; | ||
|
||
newStores.store1.trigger(); | ||
await nextTick(); | ||
expect(dartInstance.numberOfRedraws, 1, reason: 'should have redrawn from new store'); | ||
|
||
stores.store1.trigger(); | ||
await nextTick(); | ||
expect(dartInstance.numberOfRedraws, 1, reason: 'should not have redrawn from old 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.
component's