Skip to content
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
28 changes: 28 additions & 0 deletions example/test/function_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,29 @@ UseStateTestComponent(Map props) {
]);
}

var useCallbackTestFunctionComponent =
react.registerFunctionComponent(UseCallbackTestComponent, displayName: 'useCallbackTest');

UseCallbackTestComponent(Map props) {
final count = useState(0);
final delta = useState(1);

var increment = useCallback((_) {
count.setWithUpdater((prev) => prev + delta.value);
}, [delta.value]);

var incrementDelta = useCallback((_) {
delta.setWithUpdater((prev) => prev + 1);
}, []);

return react.div({}, [
react.div({}, ['Delta is ${delta.value}']),
react.div({}, ['Count is ${count.value}']),
react.button({'onClick': increment}, ['Increment count']),
react.button({'onClick': incrementDelta}, ['Increment delta']),
]);
}

void main() {
setClientConfiguration();

Expand All @@ -32,6 +55,11 @@ void main() {
useStateTestFunctionComponent({
'key': 'useStateTest',
}, []),
react.br({}),
react.h2({'key': 'useCallbackTestLabel'}, ['useCallback Hook Test']),
useCallbackTestFunctionComponent({
'key': 'useCallbackTest',
}, []),
]),
querySelector('#content'));
}
Expand Down
42 changes: 38 additions & 4 deletions lib/hooks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import 'package:react/react_client/react_interop.dart';
/// The current value of the state is available via [value] and
/// functions to update it are available via [set] and [setWithUpdater].
///
/// 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].
/// > __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].
///
/// Learn more: <https://reactjs.org/docs/hooks-state.html>.
class StateHook<T> {
Expand Down Expand Up @@ -101,3 +101,37 @@ StateHook<T> useState<T>(T initialValue) => StateHook(initialValue);
///
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#lazy-initial-state>.
StateHook<T> useStateLazy<T>(T init()) => StateHook.lazy(init);

/// Returns a memoized version of [callback] that only changes if one of the [dependencies] has changed.
///
/// > __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__:
///
/// ```
/// UseCallbackTestComponent(Map props) {
/// final count = useState(0);
/// final delta = useState(1);
///
/// var increment = useCallback((_) {
/// count.setWithUpdater((prev) => prev + delta.value);
/// }, [delta.value]);
///
/// var incrementDelta = useCallback((_) {
/// delta.setWithUpdater((prev) => prev + 1);
/// }, []);
///
/// return react.div({}, [
/// react.div({}, ['Delta is ${delta.value}']),
/// react.div({}, ['Count is ${count.value}']),
/// react.button({'onClick': increment}, ['Increment count']),
/// react.button({'onClick': incrementDelta}, ['Increment delta']),
/// ]);
/// }
/// ```
///
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#usecallback>.
Function useCallback(Function callback, List dependencies) => React.useCallback(allowInterop(callback), 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 @@ -47,6 +47,7 @@ abstract class React {
external static ReactClass forwardRef(Function(JsMap props, JsRef ref) wrapperFunction);

external static List<dynamic> useState(dynamic value);
external static Function useCallback(Function callback, List dependencies);
}

/// Creates a [Ref] object that can be attached to a [ReactElement] via the ref prop.
Expand Down
104 changes: 104 additions & 0 deletions test/hooks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,109 @@ main() {
expect(countRef.text, '1');
});
});

group('useCallback -', () {
ReactDartFunctionComponentFactoryProxy UseCallbackTest;
DivElement deltaRef;
DivElement countRef;
ButtonElement incrementWithDepButtonRef;
ButtonElement incrementNoDepButtonRef;
ButtonElement incrementDeltaButtonRef;

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

UseCallbackTest = react.registerFunctionComponent((Map props) {
final count = useState(0);
final delta = useState(1);

var incrementNoDep = useCallback((_) {
count.setWithUpdater((prev) => prev + delta.value);
}, []);

var incrementWithDep = useCallback((_) {
count.setWithUpdater((prev) => prev + delta.value);
}, [delta.value]);

var incrementDelta = useCallback((_) {
delta.setWithUpdater((prev) => prev + 1);
}, []);

return react.div({}, [
react.div({
'ref': (ref) {
deltaRef = ref;
},
}, [
delta.value
]),
react.div({
'ref': (ref) {
countRef = ref;
},
}, [
count.value
]),
react.button({
'onClick': incrementNoDep,
'ref': (ref) {
incrementNoDepButtonRef = ref;
},
}, [
'Increment count no dep'
]),
react.button({
'onClick': incrementWithDep,
'ref': (ref) {
incrementWithDepButtonRef = ref;
},
}, [
'Increment count'
]),
react.button({
'onClick': incrementDelta,
'ref': (ref) {
incrementDeltaButtonRef = ref;
},
}, [
'Increment delta'
]),
]);
});

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

tearDownAll(() {
UseCallbackTest = null;
});

test('callback is called correctly', () {
expect(countRef.text, '0');
expect(deltaRef.text, '1');

react_test_utils.Simulate.click(incrementNoDepButtonRef);
expect(countRef.text, '1');

react_test_utils.Simulate.click(incrementWithDepButtonRef);
expect(countRef.text, '2');
});

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

test('callback stays the same if state not in dependency list', () {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice tests here!

react_test_utils.Simulate.click(incrementNoDepButtonRef);
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', () {
react_test_utils.Simulate.click(incrementWithDepButtonRef);
expect(countRef.text, '5', reason: 'increments by 2 because delta updated');
});
});
});
});
}