diff --git a/example/test/function_component_test.dart b/example/test/function_component_test.dart index 2218618b..1e31c98e 100644 --- a/example/test/function_component_test.dart +++ b/example/test/function_component_test.dart @@ -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(); @@ -32,6 +55,11 @@ void main() { useStateTestFunctionComponent({ 'key': 'useStateTest', }, []), + react.br({}), + react.h2({'key': 'useCallbackTestLabel'}, ['useCallback Hook Test']), + useCallbackTestFunctionComponent({ + 'key': 'useCallbackTest', + }, []), ]), querySelector('#content')); } diff --git a/lib/hooks.dart b/lib/hooks.dart index 484de6bb..dd9ccb32 100644 --- a/lib/hooks.dart +++ b/lib/hooks.dart @@ -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 (): -/// -/// * 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: . class StateHook { @@ -101,3 +101,37 @@ StateHook useState(T initialValue) => StateHook(initialValue); /// /// Learn more: . StateHook useStateLazy(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: . +Function useCallback(Function callback, List dependencies) => React.useCallback(allowInterop(callback), dependencies); diff --git a/lib/react_client/react_interop.dart b/lib/react_client/react_interop.dart index de35244a..ae9e940e 100644 --- a/lib/react_client/react_interop.dart +++ b/lib/react_client/react_interop.dart @@ -47,6 +47,7 @@ abstract class React { external static ReactClass forwardRef(Function(JsMap props, JsRef ref) wrapperFunction); external static List 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. diff --git a/test/hooks_test.dart b/test/hooks_test.dart index e80492df..8ca6ff88 100644 --- a/test/hooks_test.dart +++ b/test/hooks_test.dart @@ -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', () { + 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'); + }); + }); + }); }); }