Skip to content

Commit 50bd3f6

Browse files
Merge pull request #233 from cleandart/CPLAT-8038-usecallback-hook
CPLAT-8038 Implement/Expose useCallback Hook
2 parents 9c615f7 + 7adbc2a commit 50bd3f6

File tree

4 files changed

+171
-4
lines changed

4 files changed

+171
-4
lines changed

example/test/function_component_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,29 @@ UseStateTestComponent(Map props) {
2121
]);
2222
}
2323

24+
var useCallbackTestFunctionComponent =
25+
react.registerFunctionComponent(UseCallbackTestComponent, displayName: 'useCallbackTest');
26+
27+
UseCallbackTestComponent(Map props) {
28+
final count = useState(0);
29+
final delta = useState(1);
30+
31+
var increment = useCallback((_) {
32+
count.setWithUpdater((prev) => prev + delta.value);
33+
}, [delta.value]);
34+
35+
var incrementDelta = useCallback((_) {
36+
delta.setWithUpdater((prev) => prev + 1);
37+
}, []);
38+
39+
return react.div({}, [
40+
react.div({}, ['Delta is ${delta.value}']),
41+
react.div({}, ['Count is ${count.value}']),
42+
react.button({'onClick': increment}, ['Increment count']),
43+
react.button({'onClick': incrementDelta}, ['Increment delta']),
44+
]);
45+
}
46+
2447
void main() {
2548
setClientConfiguration();
2649

@@ -32,6 +55,11 @@ void main() {
3255
useStateTestFunctionComponent({
3356
'key': 'useStateTest',
3457
}, []),
58+
react.br({}),
59+
react.h2({'key': 'useCallbackTestLabel'}, ['useCallback Hook Test']),
60+
useCallbackTestFunctionComponent({
61+
'key': 'useCallbackTest',
62+
}, []),
3563
]),
3664
querySelector('#content'));
3765
}

lib/hooks.dart

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import 'package:react/react_client/react_interop.dart';
1010
/// The current value of the state is available via [value] and
1111
/// functions to update it are available via [set] and [setWithUpdater].
1212
///
13-
/// Note there are two rules for using Hooks (<https://reactjs.org/docs/hooks-rules.html>):
14-
///
15-
/// * Only call Hooks at the top level.
16-
/// * Only call Hooks from inside a [DartFunctionComponent].
13+
/// > __Note:__ there are two [rules for using Hooks](https://reactjs.org/docs/hooks-rules.html):
14+
/// >
15+
/// > * Only call Hooks at the top level.
16+
/// > * Only call Hooks from inside a [DartFunctionComponent].
1717
///
1818
/// Learn more: <https://reactjs.org/docs/hooks-state.html>.
1919
class StateHook<T> {
@@ -101,3 +101,37 @@ StateHook<T> useState<T>(T initialValue) => StateHook(initialValue);
101101
///
102102
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#lazy-initial-state>.
103103
StateHook<T> useStateLazy<T>(T init()) => StateHook.lazy(init);
104+
105+
/// Returns a memoized version of [callback] that only changes if one of the [dependencies] has changed.
106+
///
107+
/// > __Note:__ there are two [rules for using Hooks](https://reactjs.org/docs/hooks-rules.html):
108+
/// >
109+
/// > * Only call Hooks at the top level.
110+
/// > * Only call Hooks from inside a [DartFunctionComponent].
111+
///
112+
/// __Example__:
113+
///
114+
/// ```
115+
/// UseCallbackTestComponent(Map props) {
116+
/// final count = useState(0);
117+
/// final delta = useState(1);
118+
///
119+
/// var increment = useCallback((_) {
120+
/// count.setWithUpdater((prev) => prev + delta.value);
121+
/// }, [delta.value]);
122+
///
123+
/// var incrementDelta = useCallback((_) {
124+
/// delta.setWithUpdater((prev) => prev + 1);
125+
/// }, []);
126+
///
127+
/// return react.div({}, [
128+
/// react.div({}, ['Delta is ${delta.value}']),
129+
/// react.div({}, ['Count is ${count.value}']),
130+
/// react.button({'onClick': increment}, ['Increment count']),
131+
/// react.button({'onClick': incrementDelta}, ['Increment delta']),
132+
/// ]);
133+
/// }
134+
/// ```
135+
///
136+
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#usecallback>.
137+
Function useCallback(Function callback, List dependencies) => React.useCallback(allowInterop(callback), dependencies);

lib/react_client/react_interop.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ abstract class React {
4747
external static ReactClass forwardRef(Function(JsMap props, JsRef ref) wrapperFunction);
4848

4949
external static List<dynamic> useState(dynamic value);
50+
external static Function useCallback(Function callback, List dependencies);
5051
}
5152

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

test/hooks_test.dart

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,109 @@ main() {
9292
expect(countRef.text, '1');
9393
});
9494
});
95+
96+
group('useCallback -', () {
97+
ReactDartFunctionComponentFactoryProxy UseCallbackTest;
98+
DivElement deltaRef;
99+
DivElement countRef;
100+
ButtonElement incrementWithDepButtonRef;
101+
ButtonElement incrementNoDepButtonRef;
102+
ButtonElement incrementDeltaButtonRef;
103+
104+
setUpAll(() {
105+
var mountNode = new DivElement();
106+
107+
UseCallbackTest = react.registerFunctionComponent((Map props) {
108+
final count = useState(0);
109+
final delta = useState(1);
110+
111+
var incrementNoDep = useCallback((_) {
112+
count.setWithUpdater((prev) => prev + delta.value);
113+
}, []);
114+
115+
var incrementWithDep = useCallback((_) {
116+
count.setWithUpdater((prev) => prev + delta.value);
117+
}, [delta.value]);
118+
119+
var incrementDelta = useCallback((_) {
120+
delta.setWithUpdater((prev) => prev + 1);
121+
}, []);
122+
123+
return react.div({}, [
124+
react.div({
125+
'ref': (ref) {
126+
deltaRef = ref;
127+
},
128+
}, [
129+
delta.value
130+
]),
131+
react.div({
132+
'ref': (ref) {
133+
countRef = ref;
134+
},
135+
}, [
136+
count.value
137+
]),
138+
react.button({
139+
'onClick': incrementNoDep,
140+
'ref': (ref) {
141+
incrementNoDepButtonRef = ref;
142+
},
143+
}, [
144+
'Increment count no dep'
145+
]),
146+
react.button({
147+
'onClick': incrementWithDep,
148+
'ref': (ref) {
149+
incrementWithDepButtonRef = ref;
150+
},
151+
}, [
152+
'Increment count'
153+
]),
154+
react.button({
155+
'onClick': incrementDelta,
156+
'ref': (ref) {
157+
incrementDeltaButtonRef = ref;
158+
},
159+
}, [
160+
'Increment delta'
161+
]),
162+
]);
163+
});
164+
165+
react_dom.render(UseCallbackTest({}), mountNode);
166+
});
167+
168+
tearDownAll(() {
169+
UseCallbackTest = null;
170+
});
171+
172+
test('callback is called correctly', () {
173+
expect(countRef.text, '0');
174+
expect(deltaRef.text, '1');
175+
176+
react_test_utils.Simulate.click(incrementNoDepButtonRef);
177+
expect(countRef.text, '1');
178+
179+
react_test_utils.Simulate.click(incrementWithDepButtonRef);
180+
expect(countRef.text, '2');
181+
});
182+
183+
group('after depending state changes,', () {
184+
setUpAll(() {
185+
react_test_utils.Simulate.click(incrementDeltaButtonRef);
186+
});
187+
188+
test('callback stays the same if state not in dependency list', () {
189+
react_test_utils.Simulate.click(incrementNoDepButtonRef);
190+
expect(countRef.text, '3', reason: 'still increments by 1 because delta not in dependency list');
191+
});
192+
193+
test('callback stays the same if state not in dependency list', () {
194+
react_test_utils.Simulate.click(incrementWithDepButtonRef);
195+
expect(countRef.text, '5', reason: 'increments by 2 because delta updated');
196+
});
197+
});
198+
});
95199
});
96200
}

0 commit comments

Comments
 (0)