Skip to content

Commit 9c615f7

Browse files
Merge pull request #223 from cleandart/CPLAT-8034-usestate-hook
CPLAT-8034 Implement/Expose useState Hook
2 parents f808ac8 + 5aa03e7 commit 9c615f7

File tree

6 files changed

+238
-30
lines changed

6 files changed

+238
-30
lines changed

example/test/function_component_test.dart

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
import 'dart:html';
22

3+
import 'package:react/hooks.dart';
34
import 'package:react/react.dart' as react;
45
import 'package:react/react_dom.dart' as react_dom;
56
import 'package:react/react_client.dart';
67

7-
import 'react_test_components.dart';
8+
var useStateTestFunctionComponent = react.registerFunctionComponent(UseStateTestComponent, displayName: 'useStateTest');
9+
10+
UseStateTestComponent(Map props) {
11+
final count = useState(0);
12+
13+
return react.div({}, [
14+
count.value,
15+
react.button({'onClick': (_) => count.set(0)}, ['Reset']),
16+
react.button({
17+
'onClick': (_) => count.setWithUpdater((prev) => prev + 1),
18+
}, [
19+
'+'
20+
]),
21+
]);
22+
}
823

924
void main() {
1025
setClientConfiguration();
11-
var inputValue = 'World';
12-
// TODO: replace this with hooks/useState when they are added.
26+
1327
render() {
1428
react_dom.render(
1529
react.Fragment({}, [
16-
react.input(
17-
{
18-
'defaultValue': inputValue,
19-
'onChange': (event) {
20-
inputValue = event.currentTarget.value;
21-
render();
22-
}
23-
},
24-
),
25-
react.br({}),
26-
helloGregFunctionComponent({'key': 'greg'}),
27-
react.br({}),
28-
helloGregFunctionComponent({'key': 'not greg'}, inputValue)
30+
react.h1({'key': 'functionComponentTestLabel'}, ['Function Component Tests']),
31+
react.h2({'key': 'useStateTestLabel'}, ['useState Hook Test']),
32+
useStateTestFunctionComponent({
33+
'key': 'useStateTest',
34+
}, []),
2935
]),
3036
querySelector('#content'));
3137
}

example/test/react_test_components.dart

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -393,16 +393,6 @@ class _NewContextTypeConsumerComponent extends react.Component2 {
393393
}
394394
}
395395

396-
var helloGregFunctionComponent = react.registerFunctionComponent(HelloGreg);
397-
398-
HelloGreg(Map props) {
399-
var content = ['Hello Greg!'];
400-
if (props['children'].isNotEmpty) {
401-
content = ['Hello ' + props['children'].join(' ') + '!'];
402-
}
403-
return react.Fragment({}, content);
404-
}
405-
406396
class _Component2TestComponent extends react.Component2 with react.TypedSnapshot<String> {
407397
get defaultProps => {'defaultProp': true};
408398

lib/hooks.dart

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
@JS()
2+
library hooks;
3+
4+
import 'package:js/js.dart';
5+
import 'package:react/react.dart';
6+
import 'package:react/react_client/react_interop.dart';
7+
8+
/// The return value of [useState].
9+
///
10+
/// The current value of the state is available via [value] and
11+
/// functions to update it are available via [set] and [setWithUpdater].
12+
///
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].
17+
///
18+
/// Learn more: <https://reactjs.org/docs/hooks-state.html>.
19+
class StateHook<T> {
20+
/// The first item of the pair returned by [React.useState].
21+
T _value;
22+
23+
/// The second item in the pair returned by [React.useState].
24+
void Function(dynamic) _setValue;
25+
26+
StateHook(T initialValue) {
27+
final result = React.useState(initialValue);
28+
_value = result[0];
29+
_setValue = result[1];
30+
}
31+
32+
/// Constructor for [useStateLazy], calls lazy version of [React.useState] to
33+
/// initialize [_value] to the return value of [init].
34+
///
35+
/// See: <https://reactjs.org/docs/hooks-reference.html#lazy-initial-state>.
36+
StateHook.lazy(T init()) {
37+
final result = React.useState(allowInterop(init));
38+
_value = result[0];
39+
_setValue = result[1];
40+
}
41+
42+
/// The current value of the state.
43+
///
44+
/// See: <https://reactjs.org/docs/hooks-reference.html#usestate>.
45+
T get value => _value;
46+
47+
/// Updates [value] to [newValue].
48+
///
49+
/// See: <https://reactjs.org/docs/hooks-state.html#updating-state>.
50+
void set(T newValue) => _setValue(newValue);
51+
52+
/// Updates [value] to the return value of [computeNewValue].
53+
///
54+
/// See: <https://reactjs.org/docs/hooks-reference.html#functional-updates>.
55+
void setWithUpdater(T computeNewValue(T oldValue)) => _setValue(allowInterop(computeNewValue));
56+
}
57+
58+
/// Adds local state to a [DartFunctionComponent]
59+
/// by returning a [StateHook] with [StateHook.value] initialized to [initialValue].
60+
///
61+
/// > __Note:__ If the [initialValue] is expensive to compute, [useStateLazy] should be used instead.
62+
///
63+
/// __Example__:
64+
///
65+
/// ```
66+
/// UseStateTestComponent(Map props) {
67+
/// final count = useState(0);
68+
///
69+
/// return react.div({}, [
70+
/// count.value,
71+
/// react.button({'onClick': (_) => count.set(0)}, ['Reset']),
72+
/// react.button({
73+
/// 'onClick': (_) => count.setWithUpdater((prev) => prev + 1),
74+
/// }, ['+']),
75+
/// ]);
76+
/// }
77+
/// ```
78+
///
79+
/// Learn more: <https://reactjs.org/docs/hooks-state.html>.
80+
StateHook<T> useState<T>(T initialValue) => StateHook(initialValue);
81+
82+
/// Adds local state to a [DartFunctionComponent]
83+
/// by returning a [StateHook] with [StateHook.value] initialized to the return value of [init].
84+
///
85+
/// __Example__:
86+
///
87+
/// ```
88+
/// UseStateTestComponent(Map props) {
89+
/// final count = useStateLazy(() {
90+
/// var initialState = someExpensiveComputation(props);
91+
/// return initialState;
92+
/// }));
93+
///
94+
/// return react.div({}, [
95+
/// count.value,
96+
/// react.button({'onClick': (_) => count.set(0)}, ['Reset']),
97+
/// react.button({'onClick': (_) => count.set((prev) => prev + 1)}, ['+']),
98+
/// ]);
99+
/// }
100+
/// ```
101+
///
102+
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#lazy-initial-state>.
103+
StateHook<T> useStateLazy<T>(T init()) => StateHook.lazy(init);

lib/react_client/react_interop.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ abstract class React {
4444
external static ReactClass get Fragment;
4545

4646
external static JsRef createRef();
47+
external static ReactClass forwardRef(Function(JsMap props, JsRef ref) wrapperFunction);
48+
49+
external static List<dynamic> useState(dynamic value);
4750
}
4851

4952
/// Creates a [Ref] object that can be attached to a [ReactElement] via the ref prop.
@@ -108,7 +111,7 @@ class JsRef {
108111
///
109112
/// See: <https://reactjs.org/docs/forwarding-refs.html>.
110113
ReactJsComponentFactoryProxy forwardRef(Function(Map props, Ref ref) wrapperFunction) {
111-
var hoc = _jsForwardRef(allowInterop((JsMap props, JsRef ref) {
114+
var hoc = React.forwardRef(allowInterop((JsMap props, JsRef ref) {
112115
final dartProps = JsBackedMap.backedBy(props);
113116
final dartRef = Ref.fromJs(ref);
114117
return wrapperFunction(dartProps, dartRef);
@@ -117,9 +120,6 @@ ReactJsComponentFactoryProxy forwardRef(Function(Map props, Ref ref) wrapperFunc
117120
return new ReactJsComponentFactoryProxy(hoc, shouldConvertDomProps: false);
118121
}
119122

120-
@JS('React.forwardRef')
121-
external ReactClass _jsForwardRef(Function(JsMap props, JsRef ref) wrapperFunction);
122-
123123
abstract class ReactDom {
124124
static Element findDOMNode(object) => ReactDOM.findDOMNode(object);
125125
static ReactComponent render(ReactElement component, Element element) => ReactDOM.render(component, element);

test/hooks_test.dart

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// ignore_for_file: deprecated_member_use_from_same_package
2+
@TestOn('browser')
3+
@JS()
4+
library hooks_test;
5+
6+
import 'dart:html';
7+
8+
import "package:js/js.dart";
9+
import 'package:react/hooks.dart';
10+
import 'package:react/react.dart' as react;
11+
import 'package:react/react_client.dart';
12+
import 'package:react/react_dom.dart' as react_dom;
13+
import 'package:react/react_test_utils.dart' as react_test_utils;
14+
import 'package:test/test.dart';
15+
16+
main() {
17+
setClientConfiguration();
18+
19+
group('React Hooks: ', () {
20+
group('useState -', () {
21+
ReactDartFunctionComponentFactoryProxy UseStateTest;
22+
DivElement textRef;
23+
DivElement countRef;
24+
ButtonElement setButtonRef;
25+
ButtonElement setWithUpdaterButtonRef;
26+
27+
setUpAll(() {
28+
var mountNode = new DivElement();
29+
30+
UseStateTest = react.registerFunctionComponent((Map props) {
31+
final text = useStateLazy(() {
32+
return 'initialValue';
33+
});
34+
final count = useState(0);
35+
36+
return react.div({}, [
37+
react.div({
38+
'ref': (ref) {
39+
textRef = ref;
40+
},
41+
}, [
42+
text.value
43+
]),
44+
react.div({
45+
'ref': (ref) {
46+
countRef = ref;
47+
},
48+
}, [
49+
count.value
50+
]),
51+
react.button({
52+
'onClick': (_) => text.set('newValue'),
53+
'ref': (ref) {
54+
setButtonRef = ref;
55+
},
56+
}, [
57+
'Set'
58+
]),
59+
react.button({
60+
'onClick': (_) => count.setWithUpdater((prev) => prev + 1),
61+
'ref': (ref) {
62+
setWithUpdaterButtonRef = ref;
63+
},
64+
}, [
65+
'+'
66+
]),
67+
]);
68+
});
69+
70+
react_dom.render(UseStateTest({}), mountNode);
71+
});
72+
73+
tearDownAll(() {
74+
UseStateTest = null;
75+
});
76+
77+
test('initializes state correctly', () {
78+
expect(countRef.text, '0');
79+
});
80+
81+
test('Lazy initializes state correctly', () {
82+
expect(textRef.text, 'initialValue');
83+
});
84+
85+
test('StateHook.set updates state correctly', () {
86+
react_test_utils.Simulate.click(setButtonRef);
87+
expect(textRef.text, 'newValue');
88+
});
89+
90+
test('StateHook.setWithUpdater updates state correctly', () {
91+
react_test_utils.Simulate.click(setWithUpdaterButtonRef);
92+
expect(countRef.text, '1');
93+
});
94+
});
95+
});
96+
}

test/hooks_test.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head lang="en">
4+
<meta charset="UTF-8">
5+
<title></title>
6+
<script src="packages/react/react.js"></script>
7+
<script src="packages/react/react_dom.js"></script>
8+
<link rel="x-dart-test" href="hooks_test.dart">
9+
<script src="packages/test/dart.js"></script>
10+
</head>
11+
<body>
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)