Skip to content

Commit 04f8a68

Browse files
Merge pull request #250 from cleandart/CPLAT-8039-useMemo-hook
CPLAT-8039 Implement/Expose useMemo Hook
2 parents b1a521b + 0c58100 commit 04f8a68

File tree

4 files changed

+223
-7
lines changed

4 files changed

+223
-7
lines changed

example/test/function_component_test.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,53 @@ UseRefTestComponent(Map props) {
183183
]);
184184
}
185185

186+
int fibonacci(int n) {
187+
if (n <= 1) {
188+
return 1;
189+
}
190+
return fibonacci(n - 1) + fibonacci(n - 2);
191+
}
192+
193+
final useMemoTestFunctionComponent = react.registerFunctionComponent(UseMemoTestComponent, displayName: 'useMemoTest');
194+
195+
UseMemoTestComponent(Map props) {
196+
final reRender = useState(0);
197+
final count = useState(35);
198+
199+
final fib = useMemo(
200+
() {
201+
print('calculating fibonacci...');
202+
return fibonacci(count.value);
203+
},
204+
205+
/// This dependency prevents [fib] from being re-calculated every time the component re-renders.
206+
[count.value],
207+
);
208+
209+
return react.Fragment({}, [
210+
react.div({'key': 'div'}, ['Fibonacci of ${count.value} is $fib']),
211+
react.button({'key': 'button1', 'onClick': (_) => count.setWithUpdater((prev) => prev + 1)}, ['+']),
212+
react.button({'key': 'button2', 'onClick': (_) => reRender.setWithUpdater((prev) => prev + 1)}, ['re-render']),
213+
]);
214+
}
215+
216+
final useMemoTestFunctionComponent2 =
217+
react.registerFunctionComponent(UseMemoTestComponent2, displayName: 'useMemoTest2');
218+
219+
UseMemoTestComponent2(Map props) {
220+
final reRender = useState(0);
221+
final count = useState(35);
222+
223+
print('calculating fibonacci...');
224+
final fib = fibonacci(count.value);
225+
226+
return react.Fragment({}, [
227+
react.div({'key': 'div'}, ['Fibonacci of ${count.value} is ${fib}']),
228+
react.button({'key': 'button1', 'onClick': (_) => count.setWithUpdater((prev) => prev + 1)}, ['+']),
229+
react.button({'key': 'button2', 'onClick': (_) => reRender.setWithUpdater((prev) => prev + 1)}, ['re-render']),
230+
]);
231+
}
232+
186233
void main() {
187234
setClientConfiguration();
188235

@@ -218,6 +265,17 @@ void main() {
218265
useRefTestFunctionComponent({
219266
'key': 'useRefTest',
220267
}, []),
268+
react.h2({'key': 'useMemoTestLabel'}, ['useMemo Hook Test']),
269+
react.h6({'key': 'h61'}, ['With useMemo:']),
270+
useMemoTestFunctionComponent({
271+
'key': 'useMemoTest',
272+
}, []),
273+
react.br({'key': 'br4'}),
274+
react.br({'key': 'br5'}),
275+
react.h6({'key': 'h62'}, ['Without useMemo (notice calculation done on every render):']),
276+
useMemoTestFunctionComponent2({
277+
'key': 'useMemoTest2',
278+
}, []),
221279
]),
222280
querySelector('#content'));
223281
}

lib/hooks.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,36 @@ T useContext<T>(Context<T> context) => ContextHelpers.unjsifyNewContext(React.us
396396
///
397397
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#useref>.
398398
Ref<T> useRef<T>([T initialValue]) => Ref.useRefInit(initialValue);
399+
400+
/// Returns a memoized version of the return value of [createFunction].
401+
///
402+
/// If one of the [dependencies] has changed, [createFunction] is run during rendering of the [DartFunctionComponent].
403+
/// This optimization helps to avoid expensive calculations on every render.
404+
///
405+
/// > __Note:__ there are two [rules for using Hooks](https://reactjs.org/docs/hooks-rules.html):
406+
/// >
407+
/// > * Only call Hooks at the top level.
408+
/// > * Only call Hooks from inside a [DartFunctionComponent].
409+
///
410+
/// __Example__:
411+
/// ```
412+
/// UseMemoTestComponent(Map props) {
413+
/// final count = useState(0);
414+
///
415+
/// final fib = useMemo(
416+
/// () => fibonacci(count.value),
417+
///
418+
/// /// This dependency prevents [fib] from being re-calculated every time the component re-renders.
419+
/// [count.value],
420+
/// );
421+
///
422+
/// return react.Fragment({}, [
423+
/// react.div({}, ['Fibonacci of ${count.value} is $fib']),
424+
/// react.button({'onClick': (_) => count.setWithUpdater((prev) => prev + 1)}, ['+']),
425+
/// ]);
426+
/// }
427+
/// ```
428+
///
429+
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#usememo>.
430+
T useMemo<T>(T Function() createFunction, [List<dynamic> dependencies]) =>
431+
React.useMemo(allowInterop(createFunction), dependencies);

lib/react_client/react_interop.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ abstract class React {
5353
external static Function useCallback(Function callback, List dependencies);
5454
external static ReactContext useContext(ReactContext context);
5555
external static JsRef useRef([dynamic initialValue]);
56+
external static dynamic useMemo(dynamic Function() createFunction, [List<dynamic> dependencies]);
5657
}
5758

5859
/// Creates a [Ref] object that can be attached to a [ReactElement] via the ref prop.

test/hooks_test.dart

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ main() {
2626
ButtonElement setWithUpdaterButtonRef;
2727

2828
setUpAll(() {
29-
var mountNode = new DivElement();
29+
var mountNode = DivElement();
3030

3131
UseStateTest = react.registerFunctionComponent((Map props) {
3232
final text = useStateLazy(() {
@@ -105,7 +105,7 @@ main() {
105105
int useEffectCleanupWithEmptyDepsCallCount;
106106

107107
setUpAll(() {
108-
mountNode = new DivElement();
108+
mountNode = DivElement();
109109
useEffectCallCount = 0;
110110
useEffectCleanupCallCount = 0;
111111
useEffectWithDepsCallCount = 0;
@@ -268,7 +268,7 @@ main() {
268268
}
269269

270270
setUpAll(() {
271-
var mountNode = new DivElement();
271+
var mountNode = DivElement();
272272

273273
UseReducerTest = react.registerFunctionComponent((Map props) {
274274
final state = useReducer(reducer, {
@@ -362,7 +362,7 @@ main() {
362362
}
363363

364364
setUpAll(() {
365-
var mountNode = new DivElement();
365+
var mountNode = DivElement();
366366

367367
UseReducerTest = react.registerFunctionComponent((Map props) {
368368
final ReducerHook<Map, Map, int> state = useReducerLazy(reducer2, props['initialCount'], initializeCount);
@@ -435,7 +435,7 @@ main() {
435435
ButtonElement incrementDeltaButtonRef;
436436

437437
setUpAll(() {
438-
var mountNode = new DivElement();
438+
var mountNode = DivElement();
439439

440440
UseCallbackTest = react.registerFunctionComponent((Map props) {
441441
final count = useState(0);
@@ -519,7 +519,7 @@ main() {
519519
expect(countRef.text, '3', reason: 'still increments by 1 because delta not in dependency list');
520520
});
521521

522-
test('callback stays the same if state not in dependency list', () {
522+
test('callback updates if state is in dependency list', () {
523523
react_test_utils.Simulate.click(incrementWithDepButtonRef);
524524
expect(countRef.text, '5', reason: 'increments by 2 because delta updated');
525525
});
@@ -589,7 +589,7 @@ main() {
589589
});
590590

591591
group('useRef -', () {
592-
var mountNode = new DivElement();
592+
var mountNode = DivElement();
593593
ReactDartFunctionComponentFactoryProxy UseRefTest;
594594
ButtonElement reRenderButton;
595595
var noInitRef;
@@ -666,6 +666,130 @@ main() {
666666
});
667667
});
668668
});
669+
670+
group('useMemo -', () {
671+
ReactDartFunctionComponentFactoryProxy UseMemoTest;
672+
StateHook<int> count;
673+
ButtonElement reRenderButtonRef;
674+
ButtonElement incrementButtonRef;
675+
676+
// Count how many times createFunction() is called for each variation of dependencies.
677+
int createFunctionCallCountWithDeps = 0;
678+
int createFunctionCallCountNoDeps = 0;
679+
int createFunctionCallCountEmptyDeps = 0;
680+
681+
// Keeps track of return value of useMemo() for each variation of dependencies.
682+
int returnValueWithDeps;
683+
int returnValueNoDeps;
684+
int returnValueEmptyDeps;
685+
686+
int fibonacci(int n) {
687+
if (n <= 1) {
688+
return 1;
689+
}
690+
return fibonacci(n - 1) + fibonacci(n - 2);
691+
}
692+
693+
setUpAll(() {
694+
final mountNode = DivElement();
695+
696+
UseMemoTest = react.registerFunctionComponent((Map props) {
697+
final reRender = useState(0);
698+
count = useState(5);
699+
700+
returnValueWithDeps = useMemo(
701+
() {
702+
createFunctionCallCountWithDeps++;
703+
return fibonacci(count.value);
704+
},
705+
[count.value],
706+
);
707+
708+
returnValueNoDeps = useMemo(
709+
() {
710+
createFunctionCallCountNoDeps++;
711+
return fibonacci(count.value);
712+
},
713+
);
714+
715+
returnValueEmptyDeps = useMemo(
716+
() {
717+
createFunctionCallCountEmptyDeps++;
718+
return fibonacci(count.value);
719+
},
720+
[],
721+
);
722+
723+
return react.Fragment({}, [
724+
react.button(
725+
{'ref': (ref) => incrementButtonRef = ref, 'onClick': (_) => count.setWithUpdater((prev) => prev + 1)},
726+
['+']),
727+
react.button({
728+
'ref': (ref) => reRenderButtonRef = ref,
729+
'onClick': (_) => reRender.setWithUpdater((prev) => prev + 1)
730+
}, [
731+
're-render'
732+
]),
733+
]);
734+
});
735+
736+
react_dom.render(UseMemoTest({}), mountNode);
737+
});
738+
739+
test('correctly initializes memoized value', () {
740+
expect(count.value, 5);
741+
742+
expect(returnValueWithDeps, 8);
743+
expect(returnValueNoDeps, 8);
744+
expect(returnValueEmptyDeps, 8);
745+
746+
expect(createFunctionCallCountWithDeps, 1);
747+
expect(createFunctionCallCountNoDeps, 1);
748+
expect(createFunctionCallCountEmptyDeps, 1);
749+
});
750+
751+
group('after depending state changes,', () {
752+
setUpAll(() {
753+
react_test_utils.Simulate.click(incrementButtonRef);
754+
});
755+
756+
test('createFunction does not run if state not in dependency list', () {
757+
expect(returnValueEmptyDeps, 8);
758+
759+
expect(createFunctionCallCountEmptyDeps, 1, reason: 'count.value is not in dependency list');
760+
});
761+
762+
test('createFunction re-runs if state is in dependency list or if there is no dependency list', () {
763+
expect(returnValueWithDeps, 13);
764+
expect(returnValueNoDeps, 13);
765+
766+
expect(createFunctionCallCountWithDeps, 2, reason: 'count.value is in dependency list');
767+
expect(createFunctionCallCountNoDeps, 2,
768+
reason: 'createFunction runs on every render because there is no dependency list');
769+
});
770+
});
771+
772+
group('after component re-renders,', () {
773+
setUpAll(() {
774+
react_test_utils.Simulate.click(reRenderButtonRef);
775+
});
776+
777+
test('createFunction re-runs if there is no dependency list', () {
778+
expect(returnValueNoDeps, 13, reason: 'count.value stayed the same so the same value is returned');
779+
780+
expect(createFunctionCallCountNoDeps, 3,
781+
reason: 'createFunction runs on every render because there is no dependency list');
782+
});
783+
784+
test('createFunction does not run if there is a dependency list', () {
785+
expect(returnValueEmptyDeps, 8);
786+
expect(returnValueWithDeps, 13);
787+
788+
expect(createFunctionCallCountEmptyDeps, 1, reason: 'no dependency changed');
789+
expect(createFunctionCallCountWithDeps, 2, reason: 'no dependency changed');
790+
});
791+
});
792+
});
669793
});
670794
}
671795

0 commit comments

Comments
 (0)