Skip to content

Commit 42df42e

Browse files
Merge pull request #730 from Workiva/fix-selector-equality
CPLAT-16997 Fix selector hooks redrawing when values haven't changed
2 parents de45468 + cb65a0e commit 42df42e

File tree

14 files changed

+863
-247
lines changed

14 files changed

+863
-247
lines changed

lib/src/component/pure_component_mixin.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ import 'package:over_react/over_react.dart';
2727
/// list of children and pass that in as shown in the example below:
2828
///
2929
/// ```dart
30-
/// import 'package:memoize/memoize.dart';
31-
///
30+
/// // imemo1 is from the `memoize` package
3231
/// final getMemoizedChildren = imemo1((items) => items.map((item) {
3332
/// return (Child()
3433
/// ..key = item.id

lib/src/over_react_redux/hooks/use_selector.dart

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ import 'package:js/js.dart';
1919
import 'package:over_react/src/over_react_redux/over_react_redux.dart';
2020
import 'package:over_react/src/over_react_redux/hooks/use_dispatch.dart';
2121
import 'package:over_react/src/util/context.dart';
22+
import 'package:over_react/src/util/dart_value_wrapper.dart';
2223
import 'package:react/react_client/react_interop.dart' show ReactContext;
2324

25+
// Notes:
26+
//
27+
// [1] This value could be either a raw value or a value wrapped in DartValueWrapper.
28+
2429
// ----------------------------------------------------
2530
// useSelector hook
2631
// ----------------------------------------------------
@@ -122,20 +127,26 @@ import 'package:react/react_client/react_interop.dart' show ReactContext;
122127
/// parameterize it with.
123128
///
124129
/// > Related: [useDispatch]
125-
TValue useSelector<TReduxState, TValue>(TValue Function(TReduxState state) selector, [
130+
TValue useSelector<TReduxState, TValue>(
131+
TValue Function(TReduxState state) selector, [
126132
bool Function(TValue tNextValue, TValue tPrevValue) equalityFn,
127133
]) {
128-
ReactInteropValue jsSelector(ReactInteropValue jsState) => wrapInteropValue(selector(unwrapInteropValue(jsState)));
134+
Object /*[1]*/ jsSelector(Object /*[1]*/ jsState) =>
135+
DartValueWrapper.wrapIfNeeded(selector(DartValueWrapper.unwrapIfNeeded(jsState)));
129136
_JsReduxStateEqualityFn jsEqualityFn = equalityFn == null
130137
? null
131138
: allowInterop((nextJsValue, prevJsValue) =>
132-
equalityFn(unwrapInteropValue(nextJsValue), unwrapInteropValue(prevJsValue)));
139+
equalityFn(DartValueWrapper.unwrapIfNeeded(nextJsValue), DartValueWrapper.unwrapIfNeeded(prevJsValue)));
133140

134-
return unwrapInteropValue<TValue>(_jsUseSelector(allowInterop(jsSelector), jsEqualityFn));
141+
if (jsEqualityFn == null) {
142+
return DartValueWrapper.unwrapIfNeeded(_jsUseSelector(allowInterop(jsSelector)));
143+
} else {
144+
return DartValueWrapper.unwrapIfNeeded(_jsUseSelector(allowInterop(jsSelector), jsEqualityFn));
145+
}
135146
}
136147

137148
@JS('ReactRedux.useSelector')
138-
external ReactInteropValue _jsUseSelector(_JsSelectorFn selector, [_JsReduxStateEqualityFn equalityFn]);
149+
external Object /*[1]*/ _jsUseSelector(_JsSelectorFn selector, [_JsReduxStateEqualityFn equalityFn]);
139150

140151
// ----------------------------------------------------
141152
// createSelectorHook
@@ -197,16 +208,22 @@ external ReactInteropValue _jsUseSelector(_JsSelectorFn selector, [_JsReduxState
197208
/// ```
198209
_SelectorFnHook<TReduxState> createSelectorHook<TReduxState>([Context context]) {
199210
final jsHook = _jsCreateSelectorHook(context?.jsThis ?? JsReactRedux.ReactReduxContext);
200-
TValue dartHook<TValue>(TValue Function(TReduxState state) selector, [
211+
TValue dartHook<TValue>(
212+
TValue Function(TReduxState state) selector, [
201213
bool Function(TValue tNextValue, TValue tPrevValue) equalityFn,
202214
]) {
203-
ReactInteropValue jsSelector(ReactInteropValue jsState) => wrapInteropValue(selector(unwrapInteropValue(jsState)));
215+
Object /*[1]*/ jsSelector(Object /*[1]*/ jsState) =>
216+
DartValueWrapper.wrapIfNeeded(selector(DartValueWrapper.unwrapIfNeeded(jsState)));
204217
_JsReduxStateEqualityFn jsEqualityFn = equalityFn == null
205218
? null
206219
: allowInterop((nextJsValue, prevJsValue) =>
207-
equalityFn(unwrapInteropValue(nextJsValue), unwrapInteropValue(prevJsValue)));
220+
equalityFn(DartValueWrapper.unwrapIfNeeded(nextJsValue), DartValueWrapper.unwrapIfNeeded(prevJsValue)));
208221

209-
return unwrapInteropValue<TValue>(jsHook(allowInterop(jsSelector), jsEqualityFn));
222+
if (jsEqualityFn == null) {
223+
return DartValueWrapper.unwrapIfNeeded(jsHook(allowInterop(jsSelector)));
224+
} else {
225+
return DartValueWrapper.unwrapIfNeeded(jsHook(allowInterop(jsSelector), jsEqualityFn));
226+
}
210227
}
211228

212229
return dartHook;
@@ -219,9 +236,9 @@ external _JsSelectorFnHook _jsCreateSelectorHook(ReactContext context);
219236
// Typedefs
220237
// ----------------------------------------------------
221238

222-
typedef _JsSelectorFn = ReactInteropValue Function(ReactInteropValue jsState);
223-
typedef _JsReduxStateEqualityFn = bool Function(ReactInteropValue nextJsValue, ReactInteropValue prevJsValue);
224-
typedef _JsSelectorFnHook = ReactInteropValue Function(_JsSelectorFn selector, [_JsReduxStateEqualityFn equalityFn]);
239+
typedef _JsSelectorFn = Object /*[1]*/ Function(Object /*[1]*/ jsState);
240+
typedef _JsReduxStateEqualityFn = bool Function(Object /*[1]*/ nextJsValue, Object /*[1]*/ prevJsValue);
241+
typedef _JsSelectorFnHook = Object /*[1]*/ Function(_JsSelectorFn selector, [_JsReduxStateEqualityFn equalityFn]);
225242
typedef _SelectorFnHook<TReduxState> = TValue Function<TValue>(
226243
TValue Function(TReduxState state) selector, [
227244
bool Function(TValue tNextValue, TValue tPrevValue) equalityFn,

lib/src/over_react_redux/over_react_redux.dart

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import 'dart:html';
2020
import 'dart:js_util' as js_util;
2121

2222
import 'package:js/js.dart';
23-
import 'package:memoize/memoize.dart';
2423
import 'package:meta/meta.dart';
2524
import 'package:over_react/component_base.dart';
2625
import 'package:over_react/src/component_declaration/annotations.dart';
2726
import 'package:over_react/src/component_declaration/builder_helpers.dart' as builder_helpers;
2827
import 'package:over_react/src/component_declaration/component_type_checking.dart';
2928
import 'package:over_react/src/util/context.dart';
29+
import 'package:over_react/src/util/dart_value_wrapper.dart';
3030
import 'package:over_react/src/util/equality.dart';
3131
import 'package:react/react_client.dart';
3232
import 'package:react/react_client/js_backed_map.dart';
@@ -35,6 +35,10 @@ import 'package:redux/redux.dart';
3535

3636
part 'over_react_redux.over_react.g.dart';
3737

38+
// Notes:
39+
//
40+
// [1] This value could be either a raw value or a value wrapped in DartValueWrapper.
41+
3842
/// This class is present:
3943
///
4044
/// 1. to allow for consumers which have used the --backwards-compat flag with over_react_codemod to statically analyze:
@@ -200,47 +204,47 @@ UiFactory<TProps> Function(UiFactory<TProps>) connect<TReduxState, TProps extend
200204
return interopFunction;
201205
}
202206

203-
JsMap handleMapStateToProps(ReactInteropValue jsState) {
207+
JsMap handleMapStateToProps(Object /*[1]*/ jsState) {
204208
return jsMapFromProps(
205209
mapStateToProps(
206-
unwrapInteropValue(jsState),
210+
DartValueWrapper.unwrapIfNeeded(jsState),
207211
),
208212
);
209213
}
210214

211-
JsMap handleMapStateToPropsWithOwnProps(ReactInteropValue jsState, JsMap jsOwnProps) {
215+
JsMap handleMapStateToPropsWithOwnProps(Object /*[1]*/ jsState, JsMap jsOwnProps) {
212216
return jsMapFromProps(
213217
mapStateToPropsWithOwnProps(
214-
unwrapInteropValue(jsState),
218+
DartValueWrapper.unwrapIfNeeded(jsState),
215219
jsPropsToTProps(jsOwnProps),
216220
),
217221
);
218222
}
219223

220-
JsMap Function(ReactInteropValue jsState) handleMakeMapStateToProps(ReactInteropValue initialJsState, JsMap initialJsOwnProps) {
224+
JsMap Function(Object /*[1]*/ jsState) handleMakeMapStateToProps(Object /*[1]*/ initialJsState, JsMap initialJsOwnProps) {
221225
var mapToFactory = makeMapStateToProps(
222-
unwrapInteropValue(initialJsState),
226+
DartValueWrapper.unwrapIfNeeded(initialJsState),
223227
jsPropsToTProps(initialJsOwnProps)
224228
);
225-
JsMap handleMakeMapStateToPropsFactory(ReactInteropValue jsState) {
229+
JsMap handleMakeMapStateToPropsFactory(Object /*[1]*/ jsState) {
226230
return jsMapFromProps(
227231
mapToFactory(
228-
unwrapInteropValue(jsState),
232+
DartValueWrapper.unwrapIfNeeded(jsState),
229233
),
230234
);
231235
}
232236
return allowInteropWithArgCount(handleMakeMapStateToPropsFactory, 1);
233237
}
234238

235-
JsMap Function(ReactInteropValue jsState, JsMap jsOwnProps) handleMakeMapStateToPropsWithOwnProps(ReactInteropValue initialJsState, JsMap initialJsOwnProps) {
239+
JsMap Function(Object /*[1]*/ jsState, JsMap jsOwnProps) handleMakeMapStateToPropsWithOwnProps(Object /*[1]*/ initialJsState, JsMap initialJsOwnProps) {
236240
var mapToFactory = makeMapStateToPropsWithOwnProps(
237-
unwrapInteropValue(initialJsState),
241+
DartValueWrapper.unwrapIfNeeded(initialJsState),
238242
jsPropsToTProps(initialJsOwnProps)
239243
);
240-
JsMap handleMakeMapStateToPropsWithOwnPropsFactory(ReactInteropValue jsState, JsMap jsOwnProps) {
244+
JsMap handleMakeMapStateToPropsWithOwnPropsFactory(Object /*[1]*/ jsState, JsMap jsOwnProps) {
241245
return jsMapFromProps(
242246
mapToFactory(
243-
unwrapInteropValue(jsState),
247+
DartValueWrapper.unwrapIfNeeded(jsState),
244248
jsPropsToTProps(jsOwnProps),
245249
),
246250
);
@@ -304,8 +308,8 @@ UiFactory<TProps> Function(UiFactory<TProps>) connect<TReduxState, TProps extend
304308
);
305309
}
306310

307-
bool handleAreStatesEqual(ReactInteropValue jsNext, ReactInteropValue jsPrev) =>
308-
areStatesEqual(unwrapInteropValue(jsNext), unwrapInteropValue(jsPrev));
311+
bool handleAreStatesEqual(Object /*[1]*/ jsNext, Object /*[1]*/ jsPrev) =>
312+
areStatesEqual(DartValueWrapper.unwrapIfNeeded(jsNext), DartValueWrapper.unwrapIfNeeded(jsPrev));
309313

310314
bool handleAreOwnPropsEqual(JsMap jsNext, JsMap jsPrev) =>
311315
areOwnPropsEqual(jsPropsToTProps(jsNext), jsPropsToTProps(jsPrev));
@@ -497,14 +501,9 @@ class ReactJsReactReduxComponentFactoryProxy extends ReactJsContextComponentFact
497501

498502
/// Converts a Redux.dart [Store] into a Javascript object formatted for consumption by react-redux.
499503
JsReactReduxStore _reduxifyStore(Store store) {
500-
// Memoize this so that the same ReactInteropValue instances will be used
501-
// for a given state, allowing JS `===` checks to not fail when the same
502-
// state object is passed.
503-
final memoizedWrapInteropValue = imemo1(wrapInteropValue);
504-
505504
return JsReactReduxStore(
506505
getState: allowInterop(() {
507-
return memoizedWrapInteropValue(store.state);
506+
return DartValueWrapper.wrapIfNeeded(store.state);
508507
}),
509508
subscribe: allowInterop((cb) {
510509
return allowInterop(store.onChange.listen((_){cb();}).cancel);
@@ -527,7 +526,7 @@ class JsReactReduxStore {
527526
external Store get dartStore;
528527

529528
external factory JsReactReduxStore({
530-
ReactInteropValue Function() getState,
529+
Object /*[1]*/ Function() getState,
531530
Dispatcher dispatch,
532531
Function Function(Function) subscribe,
533532
Store dartStore,
@@ -538,22 +537,22 @@ class JsReactReduxStore {
538537
@JS()
539538
@anonymous
540539
class JsConnectOptions {
541-
external set areStatesEqual(bool Function(ReactInteropValue, ReactInteropValue) value);
540+
external set areStatesEqual(bool Function(Object /*[1]*/, Object /*[1]*/) value);
542541
external set areOwnPropsEqual(bool Function(JsMap, JsMap) value);
543542
external set areStatePropsEqual(bool Function(JsMap, JsMap) value);
544543
external set areMergedPropsEqual(bool Function(JsMap, JsMap) value);
545544
external set forwardRef(bool value);
546545
external set pure(bool value);
547546
external set context(ReactContext value);
548-
external bool Function(ReactInteropValue, ReactInteropValue) get areStatesEqual;
547+
external bool Function(Object /*[1]*/, Object /*[1]*/) get areStatesEqual;
549548
external bool Function(JsMap, JsMap) get areOwnPropsEqual;
550549
external bool Function(JsMap, JsMap) get areStatePropsEqual;
551550
external bool Function(JsMap, JsMap) get areMergedPropsEqual;
552551
external bool get forwardRef;
553552
external bool get pure;
554553
external ReactContext get context;
555554
external factory JsConnectOptions({
556-
bool Function(ReactInteropValue, ReactInteropValue) areStatesEqual,
555+
bool Function(Object /*[1]*/, Object /*[1]*/) areStatesEqual,
557556
bool Function(JsMap, JsMap) areOwnPropsEqual,
558557
bool Function(JsMap, JsMap) areStatePropsEqual,
559558
bool Function(JsMap, JsMap) areMergedPropsEqual,
@@ -563,17 +562,3 @@ class JsConnectOptions {
563562
});
564563
}
565564

566-
/// A wrapper class that prevents dart2js from jsifying [value].
567-
class ReactInteropValue {
568-
dynamic value;
569-
}
570-
571-
/// A helper function that retrieves the [value] from a [ReactInteropValue].
572-
T unwrapInteropValue<T>(ReactInteropValue value) {
573-
return value.value as T;
574-
}
575-
576-
/// A helper function that wraps a [value] in a [ReactInteropValue].
577-
ReactInteropValue wrapInteropValue(dynamic value) {
578-
return ReactInteropValue()..value = value;
579-
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2022 Workiva Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:js';
16+
17+
// Taken from react-dart
18+
/// A wrapper around a value that can't be stored in its raw form
19+
/// within a JS object (e.g., a Dart function).
20+
class DartValueWrapper {
21+
final Object value;
22+
23+
const DartValueWrapper(this.value);
24+
25+
static final _functionWrapperCache = Expando<DartValueWrapper>('_functionWrapperCache');
26+
27+
static Object wrapIfNeeded(Object value) {
28+
// This case should be fairly uncommon, since functions usually aren't used as
29+
// a Redux store's state or the result of a connect or selector hook selector.
30+
if (value is Function && !identical(allowInterop(value), value)) {
31+
// Reuse wrappers for the same function so that they're identical to the JS.
32+
return _functionWrapperCache[value] ??= DartValueWrapper(value);
33+
}
34+
return value;
35+
}
36+
37+
static T unwrapIfNeeded<T>(Object value) {
38+
if (value is DartValueWrapper) {
39+
return value.value as T;
40+
}
41+
return value as T;
42+
}
43+
}

pubspec.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ dependencies:
1717
dart_style: ^1.2.5
1818
js: ^0.6.1+1
1919
logging: '>=0.11.3+2 <2.0.0'
20-
memoize: ^2.0.0
2120
meta: ">=1.1.6 <1.7.0" # Workaround to avoid https://github.com/dart-lang/sdk/issues/46142
2221
path: ^1.5.1
2322
react: ^6.1.4

0 commit comments

Comments
 (0)