Skip to content

Commit

Permalink
Merge pull request #406 from Workiva/FED-3114-lazy
Browse files Browse the repository at this point in the history
FED-3207 Add react lazy
  • Loading branch information
rmconsole4-wk authored Oct 7, 2024
2 parents c0da27b + 2ac0cbc commit 9a9899f
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 72 deletions.
40 changes: 12 additions & 28 deletions example/suspense/suspense.dart
Original file line number Diff line number Diff line change
@@ -1,55 +1,39 @@
@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();
final lazyComponent = react.lazy(() async {
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)
]);
}
1 change: 1 addition & 0 deletions lib/react.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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/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
2 changes: 2 additions & 0 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +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 'package:react/src/js_interop_util.dart';

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

Expand All @@ -42,6 +43,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
81 changes: 81 additions & 0 deletions lib/src/react_client/lazy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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(
Future.sync(() 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,
],
);
}, displayName: 'LazyWrapper(${_getComponentName(factory.type) ?? 'Anonymous'})');
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);
}

String? _getComponentName(Object? type) {
if (type == null) return null;

if (type is String) return type;

final name = getProperty(type, 'name');
if (name is String) return name;

final displayName = getProperty(type, 'displayName');
if (displayName is String) return displayName;

return null;
}
49 changes: 32 additions & 17 deletions test/factory/common_factory_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ 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,
bool isNonDartComponentWithDartWrapper = 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 @@ -113,7 +116,7 @@ void commonFactoryTests(ReactComponentFactoryProxy factory,
shouldAlwaysBeList: isDartComponent2(factory({})));
});

if (isDartComponent(factory({}))) {
if (isDartComponent(factory({})) && !isNonDartComponentWithDartWrapper) {
group('passes children to the Dart component when specified as', () {
final notCalledSentinelValue = Object();
dynamic childrenFromLastRender;
Expand Down Expand Up @@ -171,7 +174,7 @@ void commonFactoryTests(ReactComponentFactoryProxy factory,
}
}

if (isDartComponent2(factory({}))) {
if (isDartComponent2(factory({})) && !isNonDartComponentWithDartWrapper) {
test('executes Dart render code in the component zone', () {
final oldComponentZone = componentZone;
addTearDown(() => componentZone = oldComponentZone);
Expand All @@ -191,7 +194,10 @@ void commonFactoryTests(ReactComponentFactoryProxy factory,
}
}

void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) {
void domEventHandlerWrappingTests(
ReactComponentFactoryProxy factory, {
bool isNonDartComponentWithDartWrapper = false,
}) {
Element renderAndGetRootNode(ReactElement content) {
final mountNode = Element.div();
react_dom.render(content, mountNode);
Expand Down Expand Up @@ -268,22 +274,31 @@ void domEventHandlerWrappingTests(ReactComponentFactoryProxy factory) {
}
});

if (isDartComponent(factory({}))) {
if (isDartComponent(factory({})) && !isNonDartComponentWithDartWrapper) {
group('in a way that the handlers are callable from within the Dart component:', () {
setUpAll(() {
expect(propsFromDartRender, isNotNull,
reason: 'test setup: component must pass props into props.onDartRender');
});

late react.SyntheticMouseEvent event;
final divRef = react.createRef<DivElement>();
render(react.div({
'ref': divRef,
'onClick': (react.SyntheticMouseEvent e) => event = e,
}));
rtu.Simulate.click(divRef);
late react.SyntheticMouseEvent dummyEvent;
setUpAll(() {
final mountNode = DivElement();
document.body!.append(mountNode);
addTearDown(() {
react_dom.unmountComponentAtNode(mountNode);
mountNode.remove();
});

final dummyEvent = event;
final divRef = react.createRef<DivElement>();
react_dom.render(
react.div({
'ref': divRef,
'onClick': (react.SyntheticMouseEvent e) => dummyEvent = e,
}),
mountNode);
divRef.current!.click();
});

for (final eventCase in eventCases.where((helper) => helper.isDart)) {
test(eventCase.description, () {
Expand Down Expand Up @@ -532,7 +547,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 +592,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
Loading

0 comments on commit 9a9899f

Please sign in to comment.