Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add react lazy #406

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 11 additions & 26 deletions example/suspense/suspense.dart
Original file line number Diff line number Diff line change
@@ -1,55 +1,40 @@
@JS()
library js_components;
library example.suspense.suspense;

import 'dart:html';
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_client.dart';
import 'package:react/react_client/react_interop.dart';
import 'package:react/react_dom.dart' as react_dom;
import 'package:react/src/js_interop_util.dart';
import './simple_component.dart' deferred as simple;

@JS('React.lazy')
external ReactClass jsLazy(Promise Function() factory);

// Only intended for testing purposes, Please do not copy/paste this into repo.
// This will most likely be added to the PUBLIC api in the future,
// but needs more testing and Typing decisions to be made first.
ReactJsComponentFactoryProxy lazy(Future<ReactComponentFactoryProxy> Function() factory) =>
ReactJsComponentFactoryProxy(
jsLazy(
allowInterop(
() => futureToPromise(
// React.lazy only supports "default exports" from a module.
// This `{default: yourExport}` workaround can be found in the React.lazy RFC comments.
// See: https://github.com/reactjs/rfcs/pull/64#issuecomment-431507924
(() async => jsify({'default': (await factory()).type}))(),
),
),
),
);

main() {
final content = wrapper({});

react_dom.render(content, querySelector('#content'));
}

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

return simple.SimpleComponent;
});

var wrapper = react.registerFunctionComponent(WrapperComponent, displayName: 'wrapper');

WrapperComponent(Map props) {
final showComponent = useState(false);
return react.div({
'id': 'lazy-wrapper'
}, [
react.Suspense({'fallback': 'Loading...'}, [lazyComponent({})])
react.button({
'onClick': (_) {
showComponent.set(!showComponent.value);
}
}, 'Toggle component'),
react.Suspense({'fallback': 'Loading...'}, showComponent.value ? lazyComponent({}) : null)
]);
}
2 changes: 1 addition & 1 deletion lib/react.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ 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;
export 'package:react/react_client/react_interop.dart' show forwardRef2, createRef, memo2, 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
49 changes: 49 additions & 0 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import 'package:react/react_client/component_factory.dart' show ReactDartWrapped
import 'package:react/src/react_client/dart2_interop_workaround_bindings.dart';
import 'package:react/src/typedefs.dart';

import '../src/js_interop_util.dart';

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

// ----------------------------------------------------------------------------
Expand All @@ -42,6 +44,7 @@ abstract class React {
dynamic wrapperFunction, [
bool Function(JsMap prevProps, JsMap nextProps)? areEqual,
]);
external static ReactClass lazy(Promise Function() load);

external static bool isValidElement(dynamic object);

Expand Down Expand Up @@ -274,6 +277,52 @@ 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();
return jsify({'default': factory.type});
})(),
),
),
);

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
12 changes: 7 additions & 5 deletions test/factory/common_factory_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ import '../util.dart';
/// [dartComponentVersion] should be specified for all components with Dart render code in order to
/// properly test `props.children`, forwardRef compatibility, etc.
void commonFactoryTests(ReactComponentFactoryProxy factory,
{String? dartComponentVersion, bool skipPropValuesTest = false}) {
{String? dartComponentVersion,
bool skipPropValuesTest = false,
ReactElement Function(dynamic children)? renderWrapper}) {
_childKeyWarningTests(
factory,
renderWithUniqueOwnerName: _renderWithUniqueOwnerName,
renderWithUniqueOwnerName: (ReactElement Function() render) => _renderWithUniqueOwnerName(render, renderWrapper),
);

test('renders an instance with the corresponding `type`', () {
Expand Down Expand Up @@ -532,7 +534,7 @@ void _childKeyWarningTests(ReactComponentFactoryProxy factory,
});

test('warns when a single child is passed as a list', () {
_renderWithUniqueOwnerName(() => factory({}, [react.span({})]));
renderWithUniqueOwnerName(() => factory({}, [react.span({})]));

expect(consoleErrorCalled, isTrue, reason: 'should have outputted a warning');
expect(consoleErrorMessage, contains('Each child in a list should have a unique "key" prop.'));
Expand Down Expand Up @@ -577,12 +579,12 @@ int _nextFactoryId = 0;
/// Renders the provided [render] function with a Component2 owner that will have a unique name.
///
/// This prevents React JS from not printing key warnings it deems as "duplicates".
void _renderWithUniqueOwnerName(ReactElement Function() render) {
void _renderWithUniqueOwnerName(ReactElement Function() render, [ReactElement Function(dynamic)? wrapper]) {
final factory = react.registerComponent2(() => _UniqueOwnerHelperComponent());
factory.reactClass.displayName = 'OwnerHelperComponent_$_nextFactoryId';
_nextFactoryId++;

rtu.renderIntoDocument(factory({'render': render}));
rtu.renderIntoDocument(factory({'render': wrapper != null ? () => wrapper(render()) : render}));
}

class _UniqueOwnerHelperComponent extends react.Component2 {
Expand Down
26 changes: 26 additions & 0 deletions test/react_lazy_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@TestOn('browser')
library react.react_lazy_test;

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

import 'factory/common_factory_tests.dart';

main() {
group('lazy', () {
group('- common factory behavior -', () {
final LazyTest = react.lazy(() async => react.registerFunctionComponent((props) {
props['onDartRender']?.call(props);
return react.div({...props});
}));

commonFactoryTests(
LazyTest,
// ignore: invalid_use_of_protected_member
dartComponentVersion: ReactDartComponentVersion.component2,
renderWrapper: (child) => react.Suspense({'fallback': 'Loading...'}, child),
);
});
});
}
12 changes: 12 additions & 0 deletions test/react_lazy_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title></title>
<script src="packages/react/react_with_addons.js"></script>
<script src="packages/react/react_dom.js"></script>
<link rel="x-dart-test" href="react_lazy_test.dart" />
<script src="packages/test/dart.js"></script>
</head>
<body></body>
</html>
24 changes: 0 additions & 24 deletions test/react_suspense_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,15 @@
library react_test_utils_test;

import 'dart:html';
import 'dart:js_util';

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

import './react_suspense_lazy_component.dart' deferred as simple;

@JS('React.lazy')
external ReactClass jsLazy(Promise Function() factory);

// Only intended for testing purposes, Please do not copy/paste this into repo.
// This will most likely be added to the PUBLIC api in the future,
// but needs more testing and Typing decisions to be made first.
ReactJsComponentFactoryProxy lazy(Future<ReactComponentFactoryProxy> Function() factory) =>
ReactJsComponentFactoryProxy(
jsLazy(
allowInterop(
() => futureToPromise(
// React.lazy only supports "default exports" from a module.
// This `{default: yourExport}` workaround can be found in the React.lazy RFC comments.
// See: https://github.com/reactjs/rfcs/pull/64#issuecomment-431507924
(() async => jsify({'default': (await factory()).type}))(),
),
),
),
);

main() {
group('Suspense', () {
test('renders fallback UI first followed by the real component', () async {
Expand Down
Loading