diff --git a/lib/src/component_declaration/component_base.dart b/lib/src/component_declaration/component_base.dart index e47b99e3b..729578b7b 100644 --- a/lib/src/component_declaration/component_base.dart +++ b/lib/src/component_declaration/component_base.dart @@ -291,7 +291,7 @@ abstract class UiComponent extends react.Component imple @override Future getManagedDelayedFuture(Duration duration, T callback()) => _getDisposableProxy().getManagedDelayedFuture(duration, callback); - + @override ManagedDisposer getManagedDisposer(Disposer disposer) => _getDisposableProxy().getManagedDisposer(disposer); @@ -309,7 +309,7 @@ abstract class UiComponent extends react.Component imple {Function onError, void onDone(), bool cancelOnError}) => _getDisposableProxy().listenToStream( stream, onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); - + @override Disposable manageAndReturnDisposable(Disposable disposable) => _getDisposableProxy().manageAndReturnDisposable(disposable); @@ -327,7 +327,7 @@ abstract class UiComponent extends react.Component imple @override void manageDisposer(Disposer disposer) => _getDisposableProxy().manageDisposer(disposer); - + @override void manageStreamController(StreamController controller) => _getDisposableProxy().manageStreamController(controller); diff --git a/lib/src/experimental/built_redux_component.dart b/lib/src/experimental/built_redux_component.dart index cf0e8cefb..280069212 100644 --- a/lib/src/experimental/built_redux_component.dart +++ b/lib/src/experimental/built_redux_component.dart @@ -61,6 +61,7 @@ abstract class BuiltReduxUiComponent< @override void componentWillMount() { super.componentWillMount(); + _isDirty = false; _setUpSub(); } @@ -68,14 +69,20 @@ abstract class BuiltReduxUiComponent< @override void componentWillReceiveProps(Map nextProps) { super.componentWillReceiveProps(nextProps); - _tearDownSub(); + var tNextProps = typedPropsFactory(nextProps); + + if (tNextProps.store != props.store) { + _tearDownSub(); + _setUpSub(nextProps); + } } @mustCallSuper @override - void componentWillUpdate(Map nextProps, Map nextState) { - // _storeSub will only be null when props get updated, not on every re-render. - if (_storeSub == null) _setUpSub(nextProps); + bool shouldComponentUpdate(Map nextProps, Map nextState) { + if (isPure) return _isDirty || typedPropsFactory(nextProps).store != props.store; + + return true; } @mustCallSuper @@ -85,6 +92,20 @@ abstract class BuiltReduxUiComponent< _tearDownSub(); } + @mustCallSuper + @override + void redraw([callback()]) { + _isDirty = true; + + super.redraw(() { + _isDirty = false; + + if (callback != null) callback(); + }); + } + + bool _isDirty; + Substate _connectedState; /// The substate values of the redux store that this component subscribes to. @@ -145,6 +166,15 @@ abstract class BuiltReduxUiComponent< /// Related: [connectedState] Substate connect(V state); + /// Whether the component should be a "pure" component. + /// + /// A "pure" component will only re-render when [connectedState] is updated or [redraw] is called directly. + /// To enable this functionality, override this getter in a subclass to return `true`. When set to true it + /// is not recommended to override [redraw] or [shouldComponentUpdate]. + /// + /// Related: [shouldComponentUpdate], [redraw] + bool get isPure => false; + StreamSubscription _storeSub; void _setUpSub([Map propsMap]) { diff --git a/test/over_react/experimental/redux_component_test.dart b/test/over_react/experimental/redux_component_test.dart index 42739b77f..9b41610a0 100644 --- a/test/over_react/experimental/redux_component_test.dart +++ b/test/over_react/experimental/redux_component_test.dart @@ -14,6 +14,7 @@ import 'redux_component_test/test_reducer.dart'; part 'redux_component_test/default.dart'; part 'redux_component_test/connect.dart'; +part 'redux_component_test/pure.dart'; void main() { ReducerBuilder baseReducerBuilder; @@ -95,6 +96,78 @@ void main() { expect(component.numberOfRedraws, 1); }); + group('properly redraws when isPure is true', () { + test('when an action is triggered', () async { + var store = new Store( + baseReducerBuilder.build(), + baseState, + baseActions, + ); + var jacket = mount((TestPure()..store = store)()); + TestPureComponent component = jacket.getDartInstance(); + + store.actions.trigger1(); + await new Future.delayed(Duration.ZERO); + expect(component.numberOfRedraws, 1); + }); + + test('by not redrawing when other props change', () async { + var store = new Store( + baseReducerBuilder.build(), + baseState, + baseActions, + ); + var jacket = mount((TestPure()..store = store)()); + TestPureComponent component = jacket.getDartInstance(); + + expect(component.numberOfRedraws, 0); + + jacket.rerender((TestPure() + ..store = store + ..id = 'new id' + )()); + + expect(component.numberOfRedraws, 0); + }); + + test('by redrawing when store changes', () async { + var store = new Store( + baseReducerBuilder.build(), + baseState, + baseActions, + ); + var updatedStore = new Store( + baseReducerBuilder.build(), + new BaseState(), + new BaseActions(), + ); + var jacket = mount((TestPure()..store = store)()); + TestPureComponent component = jacket.getDartInstance(); + + expect(component.numberOfRedraws, 0); + + jacket.rerender((TestPure()..store = updatedStore)()); + + expect(component.numberOfRedraws, 1); + }); + + test('when calling redraw', () { + var store = new Store( + baseReducerBuilder.build(), + baseState, + baseActions, + ); + var jacket = mount((TestPure()..store = store)()); + TestPureComponent component = jacket.getDartInstance(); + + expect(component.numberOfRedraws, 0); + + component.redraw(); + + expect(component.numberOfRedraws, 1); + }); + }); + test('updates subscriptions when new props are passed', () async { var store = new Store( baseReducerBuilder.build(), @@ -115,9 +188,11 @@ void main() { jacket.rerender((TestDefault()..store = updatedStore)()); + expect(component.numberOfRedraws, 2); + updatedStore.actions.trigger1(); await new Future.delayed(Duration.ZERO); - expect(component.numberOfRedraws, 2); + expect(component.numberOfRedraws, 3); }); }); } diff --git a/test/over_react/experimental/redux_component_test/connect.dart b/test/over_react/experimental/redux_component_test/connect.dart index c9d16b198..cb130535c 100644 --- a/test/over_react/experimental/redux_component_test/connect.dart +++ b/test/over_react/experimental/redux_component_test/connect.dart @@ -15,14 +15,13 @@ class TestConnectComponent int numberOfRedraws = 0; @override - render() => Dom.div()(connectedState); + void componentDidUpdate(Map prevProps, Map prevState) { + numberOfRedraws++; + } @override - int connect(BaseState state) => state.count1; + render() => Dom.div()(connectedState); @override - void setState(_, [callback()]) { - numberOfRedraws++; - if (callback != null) callback(); - } + int connect(BaseState state) => state.count1; } diff --git a/test/over_react/experimental/redux_component_test/default.dart b/test/over_react/experimental/redux_component_test/default.dart index 030604e5f..90ffb6ccd 100644 --- a/test/over_react/experimental/redux_component_test/default.dart +++ b/test/over_react/experimental/redux_component_test/default.dart @@ -13,14 +13,13 @@ class TestDefaultComponent extends BuiltReduxUiComponent state; + void componentDidUpdate(Map prevProps, Map prevState) { + numberOfRedraws++; + } @override - render() => Dom.div()(); + BaseState connect(BaseState state) => state; @override - void setState(_, [callback()]) { - numberOfRedraws++; - if (callback != null) callback(); - } + render() => Dom.div()(); } diff --git a/test/over_react/experimental/redux_component_test/pure.dart b/test/over_react/experimental/redux_component_test/pure.dart new file mode 100644 index 000000000..06b97e9a1 --- /dev/null +++ b/test/over_react/experimental/redux_component_test/pure.dart @@ -0,0 +1,28 @@ +// ignore_for_file: deprecated_member_use + +part of over_react.component_declaration.redux_component_test; + +@Factory() +UiFactory TestPure; + +@Props() +class TestPureProps extends BuiltReduxUiProps {} + +@Component() +class TestPureComponent extends BuiltReduxUiComponent { + int numberOfRedraws = 0; + + @override + void componentDidUpdate(Map prevProps, Map prevState) { + numberOfRedraws++; + } + + @override + bool get isPure => true; + + @override + BaseState connect(BaseState state) => state; + + @override + render() => Dom.div()(); +}