Skip to content

Commit

Permalink
address some feedabck
Browse files Browse the repository at this point in the history
  • Loading branch information
kealjones-wk committed Sep 30, 2024
1 parent 7564c85 commit da7b412
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 67 deletions.
3 changes: 1 addition & 2 deletions example/suspense/suspense.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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/react_interop.dart';
import 'package:react/react_dom.dart' as react_dom;
import './simple_component.dart' deferred as simple;

Expand All @@ -16,7 +15,7 @@ main() {
react_dom.render(content, querySelector('#content'));
}

final lazyComponent = lazy(() async {
final lazyComponent = react.lazy(() async {
await Future.delayed(Duration(seconds: 5));
await simple.loadLibrary();

Expand Down
3 changes: 2 additions & 1 deletion lib/react.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import 'package:react/src/react_client/private_utils.dart' show validateJsApi, v
export 'package:react/src/context.dart';
export 'package:react/src/prop_validator.dart';
export 'package:react/src/react_client/event_helpers.dart';
export 'package:react/react_client/react_interop.dart' show forwardRef2, createRef, memo2, lazy;
export 'package:react/react_client/react_interop.dart' show forwardRef2, createRef, memo2;
export 'package:react/src/react_client/lazy.dart' show lazy;
export 'package:react/src/react_client/synthetic_event_wrappers.dart' hide NonNativeDataTransfer;
export 'package:react/src/react_client/synthetic_data_transfer.dart' show SyntheticDataTransfer;

Expand Down
62 changes: 1 addition & 61 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import 'package:react/react_client/js_backed_map.dart';
import 'package:react/react_client/component_factory.dart' show ReactDartWrappedComponentFactoryProxy;
import 'package:react/src/react_client/dart2_interop_workaround_bindings.dart';
import 'package:react/src/typedefs.dart';

import '../src/js_interop_util.dart';
import 'package:react/src/js_interop_util.dart';

typedef ReactJsComponentFactory = ReactElement Function(dynamic props, dynamic children);

Expand Down Expand Up @@ -277,65 +276,6 @@ ReactComponentFactoryProxy memo2(ReactComponentFactoryProxy factory,
return ReactDartWrappedComponentFactoryProxy(hoc);
}

/// Defer loading a component's code until it is rendered for the first time.
///
/// The `lazy` function is used to create lazy components in react-dart. Lazy components are able to run asynchronous code only when they are trying to be rendered for the first time, allowing for deferred loading of the component's code.
///
/// To use the `lazy` function, you need to wrap the lazy component with a `Suspense` component. The `Suspense` component allows you to specify what should be displayed while the lazy component is loading, such as a loading spinner or a placeholder.
///
/// Example usage:
/// ```dart
/// import 'package:react/react.dart' show lazy, Suspense;
/// import './simple_component.dart' deferred as simple;
///
/// final lazyComponent = lazy(() async {
/// await simple.loadLibrary();
/// return simple.SimpleComponent;
/// });
///
/// // Wrap the lazy component with Suspense
/// final app = Suspense(
/// {
/// fallback: 'Loading...',
/// },
/// lazyComponent({}),
/// );
/// ```
///
/// Defer loading a component’s code until it is rendered for the first time.
///
/// Lazy components need to be wrapped with `Suspense` to render.
/// `Suspense` also allows you to specify what should be displayed while the lazy component is loading.
ReactComponentFactoryProxy lazy(Future<ReactComponentFactoryProxy> Function() load) {
final hoc = React.lazy(
allowInterop(
() => futureToPromise(
(() async {
final factory = await load();
// By using a wrapper uiForwardRef it ensures that we have a matching factory proxy type given to react-dart's lazy,
// a `ReactDartWrappedComponentFactoryProxy`. This is necessary to have consistent prop conversions since we don't
// have access to the original factory proxy outside of this async block.
final wrapper = forwardRef2((props, ref) {
final children = props['children'];
return factory.build(
{...props, 'ref': ref},
[
if (children != null && !(children is List && children.isEmpty)) children,
],
);
});
return jsify({'default': wrapper.type});
})(),
),
),
);

// Setting this version and wrapping with ReactDartWrappedComponentFactoryProxy
// is only okay because it matches the version and factory proxy of the wrapperFactory above.
setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2);
return ReactDartWrappedComponentFactoryProxy(hoc);
}

abstract class ReactDom {
static Element? findDOMNode(ReactNode object) => ReactDOM.findDOMNode(object);
static dynamic render(ReactNode component, Element element) => ReactDOM.render(component, element);
Expand Down
68 changes: 68 additions & 0 deletions lib/src/react_client/lazy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@

import 'dart:js';
import 'dart:js_util';

import 'package:react/react.dart';
import 'package:react/react_client/component_factory.dart';
import 'package:react/react_client/react_interop.dart';
import 'package:react/src/js_interop_util.dart';

/// Defer loading a component's code until it is rendered for the first time.
///
/// The `lazy` function is used to create lazy components in react-dart. Lazy components are able to run asynchronous code only when they are trying to be rendered for the first time, allowing for deferred loading of the component's code.
///
/// To use the `lazy` function, you need to wrap the lazy component with a `Suspense` component. The `Suspense` component allows you to specify what should be displayed while the lazy component is loading, such as a loading spinner or a placeholder.
///
/// Example usage:
/// ```dart
/// import 'package:react/react.dart' show lazy, Suspense;
/// import './simple_component.dart' deferred as simple;
///
/// final lazyComponent = lazy(() async {
/// await simple.loadLibrary();
/// return simple.SimpleComponent;
/// });
///
/// // Wrap the lazy component with Suspense
/// final app = Suspense(
/// {
/// fallback: 'Loading...',
/// },
/// lazyComponent({}),
/// );
/// ```
///
/// Defer loading a component’s code until it is rendered for the first time.
///
/// Lazy components need to be wrapped with `Suspense` to render.
/// `Suspense` also allows you to specify what should be displayed while the lazy component is loading.
ReactComponentFactoryProxy lazy(Future<ReactComponentFactoryProxy> Function() load) {
final hoc = React.lazy(
allowInterop(
() => futureToPromise(
(() async {
final factory = await load();
// By using a wrapper uiForwardRef it ensures that we have a matching factory proxy type given to react-dart's lazy,
// a `ReactDartWrappedComponentFactoryProxy`. This is necessary to have consistent prop conversions since we don't
// have access to the original factory proxy outside of this async block.
final wrapper = forwardRef2((props, ref) {
final children = props['children'];
return factory.build(
{...props, 'ref': ref},
[
if (children != null && !(children is List && children.isEmpty)) children,
],
);
});
return jsify({'default': wrapper.type});
})(),
),
),
);

// Setting this version and wrapping with ReactDartWrappedComponentFactoryProxy
// is only okay because it matches the version and factory proxy of the wrapperFactory above.
// ignore: invalid_use_of_protected_member
setProperty(hoc, 'dartComponentVersion', ReactDartComponentVersion.component2);
return ReactDartWrappedComponentFactoryProxy(hoc);
}
40 changes: 40 additions & 0 deletions test/react_lazy_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
@JS()
library react.react_lazy_test;

import 'dart:async';
import 'dart:js_util';

import 'package:js/js.dart';
import 'package:react/hooks.dart';
import 'package:react/react.dart' as react;
import 'package:react/react_test_utils.dart' as rtu;
import 'package:react/react_client/component_factory.dart';
import 'package:react/react_client/react_interop.dart';
import 'package:test/test.dart';
Expand All @@ -15,6 +17,24 @@ import 'factory/common_factory_tests.dart';

main() {
group('lazy', () {
// Event more lazy behavior is tested in `react_suspense_test.dart`

test('correctly throws errors from within load function to the closest error boundary', () async {
const errorString = 'intentional future error';
final errors = [];
final errorCompleter = Completer();
final ThrowingLazyTest = react.lazy(() async { throw Exception(errorString);});
onError(error, info) {
errors.add([error, info]);
errorCompleter.complete();
}
expect(() => rtu.renderIntoDocument(_ErrorBoundary({'onComponentDidCatch': onError}, react.Suspense({'fallback': 'Loading...'}, ThrowingLazyTest({})))), returnsNormally);
await expectLater(errorCompleter.future, completes);
expect(errors, hasLength(1));
expect(errors.first.first, isA<Exception>().having((e) => e.toString(), 'message', contains(errorString)));
expect(errors.first.last, isA<ReactErrorInfo>());
});

group('Dart component', () {
final LazyTest = react.lazy(() async => react.forwardRef2((props, ref) {
useImperativeHandle(ref, () => TestImperativeHandle());
Expand Down Expand Up @@ -77,3 +97,23 @@ class TestImperativeHandle {}

@JS()
external ReactClass get _JsFoo;

final _ErrorBoundary = react.registerComponent2(() => _ErrorBoundaryComponent(), skipMethods: []);

class _ErrorBoundaryComponent extends react.Component2 {
@override
get initialState => {'hasError': false};

@override
getDerivedStateFromError(dynamic error) => {'hasError': true};

@override
componentDidCatch(dynamic error, ReactErrorInfo info) {
props['onComponentDidCatch'](error, info);
}

@override
render() {
return (state['hasError'] as bool) ? null : props['children'];
}
}
5 changes: 2 additions & 3 deletions test/react_suspense_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'dart:html';

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

Expand All @@ -15,7 +14,7 @@ import './react_suspense_lazy_component.dart' deferred as simple;
main() {
group('Suspense', () {
test('renders fallback UI first followed by the real component', () async {
final lazyComponent = lazy(() async {
final lazyComponent = react.lazy(() async {
await simple.loadLibrary();
await Future.delayed(Duration(seconds: 1));
return simple.SimpleFunctionComponent;
Expand Down Expand Up @@ -48,7 +47,7 @@ main() {
});

test('is instant after the lazy component has been loaded once', () async {
final lazyComponent = lazy(() async {
final lazyComponent = react.lazy(() async {
await simple.loadLibrary();
await Future.delayed(Duration(seconds: 1));
return simple.SimpleFunctionComponent;
Expand Down

0 comments on commit da7b412

Please sign in to comment.