Skip to content

CPLAT-8039 Implement/Expose useMemo Hook #250

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

Merged
merged 7 commits into from
Jan 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions example/test/function_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,53 @@ UseRefTestComponent(Map props) {
]);
}

int fibonacci(int n) {
if (n <= 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}

final useMemoTestFunctionComponent = react.registerFunctionComponent(UseMemoTestComponent, displayName: 'useMemoTest');

UseMemoTestComponent(Map props) {
final reRender = useState(0);
final count = useState(35);

final fib = useMemo(
() {
print('calculating fibonacci...');
return fibonacci(count.value);
},

/// This dependency prevents [fib] from being re-calculated every time the component re-renders.
[count.value],
);

return react.Fragment({}, [
react.div({'key': 'div'}, ['Fibonacci of ${count.value} is $fib']),
react.button({'key': 'button1', 'onClick': (_) => count.setWithUpdater((prev) => prev + 1)}, ['+']),
react.button({'key': 'button2', 'onClick': (_) => reRender.setWithUpdater((prev) => prev + 1)}, ['re-render']),
]);
}

final useMemoTestFunctionComponent2 =
react.registerFunctionComponent(UseMemoTestComponent2, displayName: 'useMemoTest2');

UseMemoTestComponent2(Map props) {
final reRender = useState(0);
final count = useState(35);

print('calculating fibonacci...');
final fib = fibonacci(count.value);

return react.Fragment({}, [
react.div({'key': 'div'}, ['Fibonacci of ${count.value} is ${fib}']),
react.button({'key': 'button1', 'onClick': (_) => count.setWithUpdater((prev) => prev + 1)}, ['+']),
react.button({'key': 'button2', 'onClick': (_) => reRender.setWithUpdater((prev) => prev + 1)}, ['re-render']),
]);
}

void main() {
setClientConfiguration();

Expand Down Expand Up @@ -218,6 +265,17 @@ void main() {
useRefTestFunctionComponent({
'key': 'useRefTest',
}, []),
react.h2({'key': 'useMemoTestLabel'}, ['useMemo Hook Test']),
react.h6({'key': 'h61'}, ['With useMemo:']),
useMemoTestFunctionComponent({
'key': 'useMemoTest',
}, []),
react.br({'key': 'br4'}),
react.br({'key': 'br5'}),
react.h6({'key': 'h62'}, ['Without useMemo (notice calculation done on every render):']),
useMemoTestFunctionComponent2({
'key': 'useMemoTest2',
}, []),
]),
querySelector('#content'));
}
Expand Down
33 changes: 33 additions & 0 deletions lib/hooks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,36 @@ T useContext<T>(Context<T> context) => ContextHelpers.unjsifyNewContext(React.us
///
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#useref>.
Ref<T> useRef<T>([T initialValue]) => Ref.useRefInit(initialValue);

/// Returns a memoized version of the return value of [createFunction].
///
/// If one of the [dependencies] has changed, [createFunction] is run during rendering of the [DartFunctionComponent].
/// This optimization helps to avoid expensive calculations on every render.
///
/// > __Note:__ there are two [rules for using Hooks](https://reactjs.org/docs/hooks-rules.html):
/// >
/// > * Only call Hooks at the top level.
/// > * Only call Hooks from inside a [DartFunctionComponent].
///
/// __Example__:
/// ```
/// UseMemoTestComponent(Map props) {
/// final count = useState(0);
///
/// final fib = useMemo(
/// () => fibonacci(count.value),
///
/// /// This dependency prevents [fib] from being re-calculated every time the component re-renders.
/// [count.value],
/// );
///
/// return react.Fragment({}, [
/// react.div({}, ['Fibonacci of ${count.value} is $fib']),
/// react.button({'onClick': (_) => count.setWithUpdater((prev) => prev + 1)}, ['+']),
/// ]);
/// }
/// ```
///
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#usememo>.
T useMemo<T>(T Function() createFunction, [List<dynamic> dependencies]) =>
React.useMemo(allowInterop(createFunction), dependencies);
1 change: 1 addition & 0 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ abstract class React {
external static Function useCallback(Function callback, List dependencies);
external static ReactContext useContext(ReactContext context);
external static JsRef useRef([dynamic initialValue]);
external static dynamic useMemo(dynamic Function() createFunction, [List<dynamic> dependencies]);
}

/// Creates a [Ref] object that can be attached to a [ReactElement] via the ref prop.
Expand Down
138 changes: 131 additions & 7 deletions test/hooks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ main() {
ButtonElement setWithUpdaterButtonRef;

setUpAll(() {
var mountNode = new DivElement();
var mountNode = DivElement();

UseStateTest = react.registerFunctionComponent((Map props) {
final text = useStateLazy(() {
Expand Down Expand Up @@ -105,7 +105,7 @@ main() {
int useEffectCleanupWithEmptyDepsCallCount;

setUpAll(() {
mountNode = new DivElement();
mountNode = DivElement();
useEffectCallCount = 0;
useEffectCleanupCallCount = 0;
useEffectWithDepsCallCount = 0;
Expand Down Expand Up @@ -268,7 +268,7 @@ main() {
}

setUpAll(() {
var mountNode = new DivElement();
var mountNode = DivElement();

UseReducerTest = react.registerFunctionComponent((Map props) {
final state = useReducer(reducer, {
Expand Down Expand Up @@ -362,7 +362,7 @@ main() {
}

setUpAll(() {
var mountNode = new DivElement();
var mountNode = DivElement();

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

setUpAll(() {
var mountNode = new DivElement();
var mountNode = DivElement();

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

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

group('useRef -', () {
var mountNode = new DivElement();
var mountNode = DivElement();
ReactDartFunctionComponentFactoryProxy UseRefTest;
ButtonElement reRenderButton;
var noInitRef;
Expand Down Expand Up @@ -666,6 +666,130 @@ main() {
});
});
});

group('useMemo -', () {
ReactDartFunctionComponentFactoryProxy UseMemoTest;
StateHook<int> count;
ButtonElement reRenderButtonRef;
ButtonElement incrementButtonRef;

// Count how many times createFunction() is called for each variation of dependencies.
int createFunctionCallCountWithDeps = 0;
int createFunctionCallCountNoDeps = 0;
int createFunctionCallCountEmptyDeps = 0;

// Keeps track of return value of useMemo() for each variation of dependencies.
int returnValueWithDeps;
int returnValueNoDeps;
int returnValueEmptyDeps;

int fibonacci(int n) {
if (n <= 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}

setUpAll(() {
final mountNode = DivElement();

UseMemoTest = react.registerFunctionComponent((Map props) {
final reRender = useState(0);
count = useState(5);

returnValueWithDeps = useMemo(
() {
createFunctionCallCountWithDeps++;
return fibonacci(count.value);
},
[count.value],
);

returnValueNoDeps = useMemo(
() {
createFunctionCallCountNoDeps++;
return fibonacci(count.value);
},
);

returnValueEmptyDeps = useMemo(
() {
createFunctionCallCountEmptyDeps++;
return fibonacci(count.value);
},
[],
);

return react.Fragment({}, [
react.button(
{'ref': (ref) => incrementButtonRef = ref, 'onClick': (_) => count.setWithUpdater((prev) => prev + 1)},
['+']),
react.button({
'ref': (ref) => reRenderButtonRef = ref,
'onClick': (_) => reRender.setWithUpdater((prev) => prev + 1)
}, [
're-render'
]),
]);
});

react_dom.render(UseMemoTest({}), mountNode);
});

test('correctly initializes memoized value', () {
expect(count.value, 5);

expect(returnValueWithDeps, 8);
expect(returnValueNoDeps, 8);
expect(returnValueEmptyDeps, 8);

expect(createFunctionCallCountWithDeps, 1);
expect(createFunctionCallCountNoDeps, 1);
expect(createFunctionCallCountEmptyDeps, 1);
});

group('after depending state changes,', () {
setUpAll(() {
react_test_utils.Simulate.click(incrementButtonRef);
});

test('createFunction does not run if state not in dependency list', () {
expect(returnValueEmptyDeps, 8);

expect(createFunctionCallCountEmptyDeps, 1, reason: 'count.value is not in dependency list');
});

test('createFunction re-runs if state is in dependency list or if there is no dependency list', () {
expect(returnValueWithDeps, 13);
expect(returnValueNoDeps, 13);

expect(createFunctionCallCountWithDeps, 2, reason: 'count.value is in dependency list');
expect(createFunctionCallCountNoDeps, 2,
reason: 'createFunction runs on every render because there is no dependency list');
});
});

group('after component re-renders,', () {
setUpAll(() {
react_test_utils.Simulate.click(reRenderButtonRef);
});

test('createFunction re-runs if there is no dependency list', () {
expect(returnValueNoDeps, 13, reason: 'count.value stayed the same so the same value is returned');

expect(createFunctionCallCountNoDeps, 3,
reason: 'createFunction runs on every render because there is no dependency list');
});

test('createFunction does not run if there is a dependency list', () {
expect(returnValueEmptyDeps, 8);
expect(returnValueWithDeps, 13);

expect(createFunctionCallCountEmptyDeps, 1, reason: 'no dependency changed');
expect(createFunctionCallCountWithDeps, 2, reason: 'no dependency changed');
});
});
});
});
}

Expand Down