diff --git a/lib/src/component/dom_components.dart b/lib/src/component/dom_components.dart index c5904309a..1296fcca0 100644 --- a/lib/src/component/dom_components.dart +++ b/lib/src/component/dom_components.dart @@ -325,7 +325,7 @@ abstract class Dom { /// Returns a new builder that renders a `
` tag with getters/setters for all DOM-related React props, /// optionally backed by a specified map. - static DomProps main([Map backingMap]) => DomProps(react.main as ReactComponentFactoryProxy, backingMap); + static DomProps main([Map backingMap]) => DomProps(react.htmlMain as ReactComponentFactoryProxy, backingMap); /// Returns a new builder that renders a `` tag with getters/setters for all DOM-related React props, /// optionally backed by a specified map. diff --git a/lib/src/component/hooks.dart b/lib/src/component/hooks.dart index e54caa073..6b4c6d3e9 100644 --- a/lib/src/component/hooks.dart +++ b/lib/src/component/hooks.dart @@ -321,7 +321,49 @@ T useContext(Context context) => react_hooks.useContext(context.reactDartC /// ``` /// /// Learn more: . -Ref useRef([T initialValue]) => react_hooks.useRef(initialValue); +Ref useRef([ + // This will eventually be deprecated, but not just yet. + // @Deprecated('Use `useRefInit` instead to create refs with initial values.' + // ' Since the argument to useRefInit is required, it can be used to create a Ref that holds a non-nullable type,' + // ' whereas this function can only create Refs with nullable type arguments.') + T initialValue, +]) => + react_hooks.useRef(initialValue); + +/// Returns a mutable [Ref] object with [Ref.current] property initialized to [initialValue]. +/// +/// Changes to the [Ref.current] property do not cause the containing [uiFunction] to re-render. +/// +/// The returned [Ref] object will persist for the full lifetime of the [uiFunction]. +/// Compare to [createRef] which returns a new [Ref] object on each render. +/// +/// > __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 [uiFunction]. +/// +/// __Example__: +/// +/// ```dart +/// UiFactory UseRefExample = uiFunction( +/// (props) { +/// final countRef = useRefInit(0); +/// +/// handleClick([_]) { +/// ref.current = ref.current + 1; +/// window.alert('You clicked ${ref.current} times!'); +/// } +/// +/// return Fragment()( +/// (Dom.button()..onClick = handleClick)('Click me!'), +/// ); +/// }, +/// _$UseRefExampleConfig, // ignore: undefined_identifier +/// ); +/// ``` +/// +/// Learn more: . +Ref useRefInit(T initialValue) => react_hooks.useRefInit(initialValue); /// Returns a memoized version of the return value of [createFunction]. /// diff --git a/lib/src/component/react_dart_deprecated_forward_ref.dart b/lib/src/component/react_dart_deprecated_forward_ref.dart new file mode 100644 index 000000000..032486542 --- /dev/null +++ b/lib/src/component/react_dart_deprecated_forward_ref.dart @@ -0,0 +1,51 @@ + +import 'dart:js_util'; + +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; +import 'package:react/react_client/component_factory.dart'; +import 'package:react/react_client/js_backed_map.dart'; +import 'package:react/react_client/js_interop_helpers.dart'; +import 'package:react/react_client/react_interop.dart'; +import 'package:react/react_client/zone.dart'; + +/// A copy of the `forwardRef` function that was removed from react-dart in 7.0.0, +/// but is still needed by the deprecated `forwardRef` in over_react. +/// +/// We'll remove this copy once we remove over_react's forwardRef in the next major. +@internal +ReactJsComponentFactoryProxy forwardRef( + Function(Map props, Ref ref) wrapperFunction, { + String displayName = 'Anonymous', +}) { + // ignore: invalid_use_of_visible_for_testing_member + final wrappedComponent = allowInterop((JsMap props, ref) => componentZone.run(() { + final dartProps = JsBackedMap.backedBy(props); + for (final value in dartProps.values) { + if (value is Function) { + // Tag functions that came straight from the JS + // so that we know to pass them through as-is during prop conversion. + isRawJsFunctionFromProps[value] = true; + } + } + + final dartRef = Ref.fromJs(ref as JsRef); + return wrapperFunction(dartProps, dartRef); + })); + defineProperty(wrappedComponent, 'displayName', JsPropertyDescriptor(value: displayName)); + + final hoc = React.forwardRef(wrappedComponent); + // ignore: invalid_use_of_protected_member + setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2); + + return ReactJsComponentFactoryProxy(hoc, alwaysReturnChildrenAsList: true); +} + +@JS() +@anonymous +class JsPropertyDescriptor { + external factory JsPropertyDescriptor({dynamic value}); +} + +@JS('Object.defineProperty') +external void defineProperty(dynamic object, String propertyName, JsPropertyDescriptor descriptor); diff --git a/lib/src/component/ref_util.dart b/lib/src/component/ref_util.dart index 4077503e6..cfc01bda1 100644 --- a/lib/src/component/ref_util.dart +++ b/lib/src/component/ref_util.dart @@ -22,6 +22,8 @@ import 'package:react/react_client/react_interop.dart' as react_interop; import 'package:react/react_client.dart'; import 'package:over_react/component_base.dart'; +import './react_dart_deprecated_forward_ref.dart' as react_dart_old_forward_ref; + /// Creates a [Ref] object that can be attached to a [ReactElement] via the ref prop. /// /// __Example__: @@ -224,7 +226,7 @@ UiFactory Function(UiFactory) forwardRef return wrapperFunction(factory(props), ref); } - ReactComponentFactoryProxy hoc = react_interop.forwardRef(wrapProps, displayName: displayName); + final hoc = react_dart_old_forward_ref.forwardRef(wrapProps, displayName: displayName); setComponentTypeMeta(hoc.type, isHoc: true, parentType: factory().componentFactory.type); TProps forwardedFactory([Map props]) { diff --git a/lib/src/component_declaration/component_base_2.dart b/lib/src/component_declaration/component_base_2.dart index 948e06696..3381f008f 100644 --- a/lib/src/component_declaration/component_base_2.dart +++ b/lib/src/component_declaration/component_base_2.dart @@ -26,7 +26,6 @@ import 'package:react/react.dart' as react; import 'package:react/react_client.dart'; import 'package:react/react_client/bridge.dart'; import 'package:react/react_client/js_backed_map.dart'; -import 'package:react/react_client/react_interop.dart'; import 'builder_helpers.dart'; import 'component_type_checking.dart'; @@ -708,13 +707,13 @@ class UiComponent2BridgeImpl extends Component2BridgeImpl { JsMap jsifyPropTypes( covariant UiComponent2 component, covariant Map*/Function> propTypes) { - Error _getErrorFromConsumerValidator( + Error/*?*/ _getErrorFromConsumerValidator( /*PropValidator*/Function _validator, - JsBackedMap _props, + Map _props, react.PropValidatorInfo _info, ) { - var convertedProps = component.typedPropsFactoryJs(_props); - return _validator(convertedProps, _info) as Error; + final typedProps = component.typedPropsFactory(_props); + return _validator(typedProps, _info) as Error/*?*/; } // Add [PropValidator]s for props annotated as required. @@ -723,18 +722,15 @@ class UiComponent2BridgeImpl extends Component2BridgeImpl { consumedProps.props.forEach((prop) { if (!prop.isRequired) return; - Error requiredPropValidator( + Error/*?*/ requiredPropValidator( Map _props, react.PropValidatorInfo _info, ) { - Error consumerError; + Error/*?*/ consumerError; // Check if the consumer has specified a propType for this key. - if (propTypes[prop.key] != null) { - consumerError = _getErrorFromConsumerValidator( - propTypes[prop.key], - JsBackedMap.from(_props), - _info, - ); + final propType = propTypes[prop.key]; + if (propType != null) { + consumerError = _getErrorFromConsumerValidator(propType, _props, _info); } if (consumerError != null) return consumerError; @@ -753,29 +749,15 @@ class UiComponent2BridgeImpl extends Component2BridgeImpl { }); }); - // Wrap consumer-provided and required validators with ones that convert plain props maps into typed ones. - return JsBackedMap.from(newPropTypes.map((_propKey, _validator) { - // ignore: prefer_function_declarations_over_variables - JsPropValidator handlePropValidator = ( - JsMap _props, // ignore: avoid_types_on_closure_parameters - String _propName, // ignore: avoid_types_on_closure_parameters - String _componentName, // ignore: avoid_types_on_closure_parameters - String _location, // ignore: avoid_types_on_closure_parameters - String _propFullName, // ignore: avoid_types_on_closure_parameters - // This is a required argument of PropTypes validators but is hidden from the JS consumer. - String secret, // ignore: avoid_types_on_closure_parameters - ) { - final error = _getErrorFromConsumerValidator( - _validator, - JsBackedMap.fromJs(_props), - react.PropValidatorInfo( - propName: _propName, componentName: _componentName, location: _location, propFullName: _propFullName), - ); - return error == null ? null : JsError(error.toString()); - }; - - return MapEntry(_propKey, allowInterop(handlePropValidator)); - })).jsObject; + return super.jsifyPropTypes(component, newPropTypes.map((propKey, validator) { + // Use a LHS-typed function variable so we can easily ensure that the function is typed a + // specific way (especially since the typing can't be inferred due to the use of `Function`). + // ignore: prefer_function_declarations_over_variables + final react.PropValidator wrappedValidator = (props, info) { + return _getErrorFromConsumerValidator(validator, props, info); + }; + return MapEntry(propKey, wrappedValidator); + })); } /// A version of [setStateWithTypedUpdater] whose updater is passed typed views diff --git a/lib/src/over_react_redux/over_react_redux.dart b/lib/src/over_react_redux/over_react_redux.dart index 1ecd43c51..80f76c089 100644 --- a/lib/src/over_react_redux/over_react_redux.dart +++ b/lib/src/over_react_redux/over_react_redux.dart @@ -479,7 +479,7 @@ class ReactJsReactReduxComponentFactoryProxy extends ReactJsContextComponentFact }) : super(jsClass, isProvider: isProvider, isConsumer: isConsumer, shouldConvertDomProps: shouldConvertDomProps); @override - ReactElement build(Map props, [List childrenArgs]) { + ReactElement build(Map props, [List childrenArgs = const []]) { return super.build(_generateReduxJsProps(props), childrenArgs); } diff --git a/pubspec.yaml b/pubspec.yaml index 55b4da9d3..d968b94bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: logging: ^1.0.0 meta: ^1.6.0 path: ^1.5.1 - react: ^6.2.0 + react: ^6.3.0 redux: '>=3.0.0 <6.0.0' source_span: ^1.4.1 transformer_utils: ^0.2.6 diff --git a/test/over_react/util/component_debug_name_test.dart b/test/over_react/util/component_debug_name_test.dart index dee9c26ec..21bed3c08 100644 --- a/test/over_react/util/component_debug_name_test.dart +++ b/test/over_react/util/component_debug_name_test.dart @@ -21,18 +21,6 @@ void main() { expect(getDebugNameForDartComponent(component), 'TestComponent2'); }); }); - - group('returns the .displayName getter for a non-mounted component', () { - test('UiComponent component declared with standard boilerplate', () { - final component = TestNonMountedComponentComponent(); - expect(getDebugNameForDartComponent(component), component.displayName); - }); - - test('UiComponent2 component declared with standard boilerplate', () { - final component = TestNonMountedComponent2Component(); - expect(getDebugNameForDartComponent(component), component.displayName); - }); - }); }); } diff --git a/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart b/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart index 04c5a7ea6..bbf200217 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/exhaustive_deps.dart @@ -546,7 +546,8 @@ class ExhaustiveDeps extends DiagnosticContributor { } // useRef() return value is stable. - if (init.tryCast()?.function.tryCast()?.name == 'useRef') { + if (const {'useRef', 'useRefInit'} + .contains(init.tryCast()?.function.tryCast()?.name)) { return true; } diff --git a/tools/analyzer_plugin/test/integration/diagnostics/exhaustive_deps_test_cases.dart b/tools/analyzer_plugin/test/integration/diagnostics/exhaustive_deps_test_cases.dart index f59862863..e67193952 100644 --- a/tools/analyzer_plugin/test/integration/diagnostics/exhaustive_deps_test_cases.dart +++ b/tools/analyzer_plugin/test/integration/diagnostics/exhaustive_deps_test_cases.dart @@ -607,6 +607,17 @@ final Map>> tests = { }, null); ''', }, + { + 'name': 'Ref from useRefInit', + 'code': r''' + final MyComponent = uiFunction((_) { + final ref = useRefInit(0); + useEffect(() { + print(ref.current); + }, []); + }, null); + ''', + }, { 'name': 'Ref from useRef (namespaced)', 'code': r''' @@ -618,6 +629,17 @@ final Map>> tests = { }, null); ''', }, + { + 'name': 'Ref from useRefInit (namespaced)', + 'code': r''' + final MyComponent = uiFunction((_) { + final ref = over_react.useRefInit(0); + useEffect(() { + print(ref.current); + }, []); + }, null); + ''', + }, { 'code': r''' StateHook useFunnyState(T initialState) => useState(initialState);