Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
38 changes: 22 additions & 16 deletions example/test/function_component_test.dart
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import 'dart:html';

import 'package:react/hooks.dart';
import 'package:react/react.dart' as react;
import 'package:react/react_dom.dart' as react_dom;
import 'package:react/react_client.dart';

import 'react_test_components.dart';
var useStateTestFunctionComponent = react.registerFunctionComponent(UseStateTestComponent, displayName: 'useStateTest');

UseStateTestComponent(Map props) {
final count = useState(0);

return react.div({}, [
count.value,
react.button({'onClick': (_) => count.set(0)}, ['Reset']),
react.button({
'onClick': (_) => count.setWithUpdater((prev) => prev + 1),
}, [
'+'
]),
]);
}

void main() {
setClientConfiguration();
var inputValue = 'World';
// TODO: replace this with hooks/useState when they are added.

render() {
react_dom.render(
react.Fragment({}, [
react.input(
{
'defaultValue': inputValue,
'onChange': (event) {
inputValue = event.currentTarget.value;
render();
}
},
),
react.br({}),
helloGregFunctionComponent({'key': 'greg'}),
react.br({}),
helloGregFunctionComponent({'key': 'not greg'}, inputValue)
react.h1({'key': 'functionComponentTestLabel'}, ['Function Component Tests']),
react.h2({'key': 'useStateTestLabel'}, ['useState Hook Test']),
useStateTestFunctionComponent({
'key': 'useStateTest',
}, []),
]),
querySelector('#content'));
}
Expand Down
10 changes: 0 additions & 10 deletions example/test/react_test_components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -393,16 +393,6 @@ class _NewContextTypeConsumerComponent extends react.Component2 {
}
}

var helloGregFunctionComponent = react.registerFunctionComponent(HelloGreg);

HelloGreg(Map props) {
var content = ['Hello Greg!'];
if (props['children'].isNotEmpty) {
content = ['Hello ' + props['children'].join(' ') + '!'];
}
return react.Fragment({}, content);
}

class _Component2TestComponent extends react.Component2 with react.TypedSnapshot<String> {
get defaultProps => {'defaultProp': true};

Expand Down
103 changes: 103 additions & 0 deletions lib/hooks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
@JS()
library hooks;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a reason we are making hooks a standalone public entrypoint? Any reason to not move this to lib/src/hooks.dart and then export that from lib/react.dart like we are doing for the context APIs?

Copy link
Collaborator

Choose a reason for hiding this comment

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

After going back and forth on it, I suggested this offline to makes things a little more organized as opposed to just lumping more things into react.dart. Also, since they're APIs that will only be used sometimes, I figured it made sense for them to be their own self-contained entrypoint.

But, I could be convinced to move them into react.dart.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel the same it makes sense to have them seperate but i also enjoy the convenience of getting them when importing react.dart... plus they would get tree shaken out if they weren't used right?


import 'package:js/js.dart';
import 'package:react/react.dart';
import 'package:react/react_client/react_interop.dart';

/// The return value of [useState].
///
/// 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].
///
/// Learn more: <https://reactjs.org/docs/hooks-state.html>.
class StateHook<T> {
/// The first item of the pair returned by [React.useState].
T _value;

/// The second item in the pair returned by [React.useState].
void Function(dynamic) _setValue;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe the argument here should be typed as T.

Suggested change
void Function(dynamic) _setValue;
void Function(T newValue) _setValue;

Copy link
Collaborator

Choose a reason for hiding this comment

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

You might be right... my brain hurts when thinking about Dart 2 function types haha, so I'm not sure.

Works for me so long as there are no type errors in DDC or dart2js!

Copy link
Collaborator Author

@sydneyjodon-wk sydneyjodon-wk Nov 6, 2019

Choose a reason for hiding this comment

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

Changing this type breaks the tests for setTx. I think it's because _setValue is the updater function returned by the JS useState and can take an argument of type T newValue or type T computeNewValue(T oldValue).

Copy link
Collaborator

Choose a reason for hiding this comment

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


StateHook(T initialValue) {
final result = React.useState(initialValue);
_value = result[0];
_setValue = result[1];
}

/// Constructor for [useStateLazy], calls lazy version of [React.useState] to
/// initialize [_value] to the return value of [init].
///
/// See: <https://reactjs.org/docs/hooks-reference.html#lazy-initial-state>.
StateHook.lazy(T init()) {
final result = React.useState(allowInterop(init));
_value = result[0];
_setValue = result[1];
}

/// The current value of the state.
///
/// See: <https://reactjs.org/docs/hooks-reference.html#usestate>.
T get value => _value;

/// Updates [value] to [newValue].
///
/// See: <https://reactjs.org/docs/hooks-state.html#updating-state>.
void set(T newValue) => _setValue(newValue);

/// Updates [value] to the return value of [computeNewValue].
///
/// See: <https://reactjs.org/docs/hooks-reference.html#functional-updates>.
void setWithUpdater(T computeNewValue(T oldValue)) => _setValue(allowInterop(computeNewValue));
}

/// Adds local state to a [DartFunctionComponent]
/// by returning a [StateHook] with [StateHook.value] initialized to [initialValue].
///
/// > __Note:__ If the [initialValue] is expensive to compute, [useStateLazy] should be used instead.
///
/// __Example__:
///
/// ```
/// UseStateTestComponent(Map props) {
/// final count = useState(0);
///
/// return react.div({}, [
/// count.value,
/// react.button({'onClick': (_) => count.set(0)}, ['Reset']),
/// react.button({
/// 'onClick': (_) => count.setWithUpdater((prev) => prev + 1),
/// }, ['+']),
/// ]);
/// }
/// ```
///
/// Learn more: <https://reactjs.org/docs/hooks-state.html>.
StateHook<T> useState<T>(T initialValue) => StateHook(initialValue);

/// Adds local state to a [DartFunctionComponent]
/// by returning a [StateHook] with [StateHook.value] initialized to the return value of [init].
///
/// __Example__:
///
/// ```
/// UseStateTestComponent(Map props) {
/// final count = useStateLazy(() {
/// var initialState = someExpensiveComputation(props);
/// return initialState;
/// }));
///
/// return react.div({}, [
/// count.value,
/// react.button({'onClick': (_) => count.set(0)}, ['Reset']),
/// react.button({'onClick': (_) => count.set((prev) => prev + 1)}, ['+']),
/// ]);
/// }
/// ```
///
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#lazy-initial-state>.
StateHook<T> useStateLazy<T>(T init()) => StateHook.lazy(init);
8 changes: 4 additions & 4 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ abstract class React {
external static ReactClass get Fragment;

external static JsRef createRef();
external static ReactClass forwardRef(Function(JsMap props, JsRef ref) wrapperFunction);

external static List<dynamic> useState(dynamic value);
}

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

@JS('React.forwardRef')
external ReactClass _jsForwardRef(Function(JsMap props, JsRef ref) wrapperFunction);

abstract class ReactDom {
static Element findDOMNode(object) => ReactDOM.findDOMNode(object);
static ReactComponent render(ReactElement component, Element element) => ReactDOM.render(component, element);
Expand Down
96 changes: 96 additions & 0 deletions test/hooks_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// ignore_for_file: deprecated_member_use_from_same_package
@TestOn('browser')
@JS()
library hooks_test;

import 'dart:html';

import "package:js/js.dart";
import 'package:react/hooks.dart';
import 'package:react/react.dart' as react;
import 'package:react/react_client.dart';
import 'package:react/react_dom.dart' as react_dom;
import 'package:react/react_test_utils.dart' as react_test_utils;
import 'package:test/test.dart';

main() {
setClientConfiguration();

group('React Hooks: ', () {
group('useState -', () {
ReactDartFunctionComponentFactoryProxy UseStateTest;
DivElement textRef;
DivElement countRef;
ButtonElement setButtonRef;
ButtonElement setWithUpdaterButtonRef;

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

UseStateTest = react.registerFunctionComponent((Map props) {
final text = useStateLazy(() {
return 'initialValue';
});
final count = useState(0);

return react.div({}, [
react.div({
'ref': (ref) {
textRef = ref;
},
}, [
text.value
]),
react.div({
'ref': (ref) {
countRef = ref;
},
}, [
count.value
]),
react.button({
'onClick': (_) => text.set('newValue'),
'ref': (ref) {
setButtonRef = ref;
},
}, [
'Set'
]),
react.button({
'onClick': (_) => count.setWithUpdater((prev) => prev + 1),
'ref': (ref) {
setWithUpdaterButtonRef = ref;
},
}, [
'+'
]),
]);
});

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

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

test('initializes state correctly', () {
expect(countRef.text, '0');
});

test('Lazy initializes state correctly', () {
expect(textRef.text, 'initialValue');
});

test('StateHook.set updates state correctly', () {
react_test_utils.Simulate.click(setButtonRef);
expect(textRef.text, 'newValue');
});

test('StateHook.setWithUpdater updates state correctly', () {
react_test_utils.Simulate.click(setWithUpdaterButtonRef);
expect(countRef.text, '1');
});
});
});
}
13 changes: 13 additions & 0 deletions test/hooks_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="packages/react/react.js"></script>
<script src="packages/react/react_dom.js"></script>
<link rel="x-dart-test" href="hooks_test.dart">
<script src="packages/test/dart.js"></script>
</head>
<body>
</body>
</html>