Workiva / over_react

A library for building statically-typed React UI components using Dart.
Other
427 stars 58 forks source link

OverReact

A library for building statically-typed React UI components using Dart.

This library also exposes OverReact Redux, which has its own documentation.

Pub Documentation OverReact Analyzer Plugin (beta) Join the gitter chat

Dart CI


Migration Guides

Null safety

over_react 5.0.0 introduces support for null safety.

Now, you can declare non-nullable required props, using the late keyword. See the docs null safety and required props for more information.

To migrate components to null safety, see our null safety migration guide.

More Migration Guides

   

Using OverReact

Prerequisites

  • Familiarize yourself with React JS

    Since OverReact is built atop React JS, we strongly encourage you to gain familiarity with it by reading some React JS tutorials first.

  • Familiarize yourself with Dart Web applications

    If you have never built a Web application in Dart, we strongly encourage you to gain familiarity with the core terminology, tools and boilerplate necessary to serve an application locally using Dart. Dart has fantastic documentation and tutorials to get you started.

  1. Add the over_react package as a dependency in your pubspec.yaml.

    dependencies:
      over_react: ^4.0.0
  2. Enable the OverReact Analyzer Plugin (beta), which has many lints and assists to make authoring OverReact components easier!

  3. Include the native JavaScript react and react_dom libraries in your app’s index.html file, and add an HTML element with a unique identifier where you’ll mount your OverReact UI component(s).

    <html>
      <head>
        <!-- ... -->
      </head>
      <body>
        <div id="react_mount_point">
          // OverReact component render() output will show up here.
        </div>
    
        <script src="https://github.com/Workiva/over_react/raw/master/packages/react/react.js"></script>
        <script src="https://github.com/Workiva/over_react/raw/master/packages/react/react_dom.js"></script>
    
        <!-- NOTE: "index" should correspond to the
             name of the `.dart` file that contains your `main()` entrypoint. -->
        <script type="application/javascript" defer src="https://github.com/Workiva/over_react/raw/master/index.dart.js"></script>
      </body>
    </html>

    Note: When serving your application in production, use packages/react/react_with_react_dom_prod.js file instead of the un-minified react.js / react_dom.js files shown in the example above.

  4. Import the over_react and react_dom libraries into index.dart. Then build some components and mount / render a React tree within the HTML element you created in the previous step by calling react_dom.render() within the main() entrypoint of your Dart application.

    Be sure to namespace the react_dom.dart import as react_dom to avoid collisions with UiComponent.render when creating custom components.

    import 'dart:html';
    import 'package:over_react/react_dom.dart' as react_dom;
    import 'package:over_react/over_react.dart';
    
    // Example of where the `Foo` component might be exported from
    import 'package:your_package_name/foo.dart';
    
    main() {
      // Mount / render your component/application.
      react_dom.render(
        Foo()(),
        querySelector('#react_mount_point'),
      );
    }
  5. Run webdev serve in the root of your Dart project.

Note: If you're not using the latest component boilerplate, you'll have to restart your analysis server in your IDE for the built types to resolve properly after the build completes. Unfortunately, this is a known limitation in the analysis server at this time.

Migrate your components to the latest component boilerplate to never worry about this again!

 

Running unit tests in your project

When running unit tests on code that uses the overreact builder (or any code that imports over_react)_, __you must run your tests using the build_runner package__.

Warning: Do not run tests via pub run build_runner test in a package while another instance of build_runner (e.g. pub run build_runner serve) is running in that same package. This workflow is unsupported by build_runner

  1. Run tests using the build_runner package, and specify the platform to be a browser platform. Example:

    $ pub run build_runner test -- -p chrome test/your_test_file.dart

OverReact Component Unit Test Examples

Below are links to a UI component from our example "Todo App", and its analogous tests that we've written for components we use in . We utilize the utilities found in our over_react_test library.

   

Anatomy of an OverReact component

If you are not familiar with React JS

Since OverReact is built atop React JS, we strongly encourage you to gain familiarity with it by reading this React JS tutorial first.

The over_react library functions as an additional "layer" atop the Dart react package which handles the underlying JS interop that wraps around React JS.

The library strives to maintain a 1:1 relationship with the React JS component class and API. To do that, an OverReact component is comprised of four core pieces that are each wired up via our builder.

  1. UiFactory
  2. UiProps
  3. component, either a:
    1. function component uiFunction
    2. class component UiComponent2 (and optionally a UiState)

 

UiFactory

UiFactory is a function that returns a new instance of a component's UiProps class.

// Class component
UiFactory<FooProps> Foo = castUiFactory(_$Foo);

// Function component
UiFactory<FooProps> Foo = uiFunction((props) { /*...*/ }, _$FooConfig);

 

UiProps

UiProps is a Map class that adds statically-typed getters and setters for each React component prop. It can also be invoked as a function, serving as a builder for its analogous component.

mixin FooProps on UiProps {
  // ... the props for your component go here
  String? bar;
  bool? baz;
  List<int>? bizzles;
}

 

With other mixins

To compose props mixin classes, create a class alias that uses UiProps as the base and mix in multiple props mixins. The generated props implementation will then use it as the base class and implement the generated version of those props mixins.

UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier

mixin FooPropsMixin on UiProps {
  String? bar;
  bool? baz;
  List<int>? bizzles;
}

class FooProps = UiProps with FooPropsMixin, BarPropsMixin;

class FooComponent extends UiComponent2<FooProps> {
  // ...
}
Composition

The use-case for composing multiple props mixins into a single component props class is typically a component that renders another component, and therefore needs to expose the prop interface of that child component which will get forwarded via addUnconsumedProps.

Check out an example of props mixin component composition here

 

UiProps as a Map

[!WARNING] Directly reading late required props on arbitrary maps is unsafe.

See the docs on Unsafe Required Prop Reads for more information and for instructions on how to read these props safely.

UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier

mixin FooProps on UiProps {
  String? color;
}

class FooComponent extends UiComponent2<FooProps> {
  // ...
}

void bar() {
  FooProps props = Foo();

  props.color = '#66cc00';

  print(props.color); // #66cc00
  print(props);       // {FooProps.color: #66cc00}
}

/// You can also use the factory to create a UiProps instance
/// backed by an existing Map.
void baz() {
  Map existingMap = {'FooProps.color': '#0094ff'};

  FooProps props = Foo(existingMap);

  print(props.color); // #0094ff
}

 

UiProps as a builder

UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier

mixin FooProps on UiProps {
  String? color;
}

class FooComponent extends UiComponent2<FooProps> {
  ReactElement bar() {
    // Create a UiProps instance to serve as a builder
    FooProps builder = Foo();

    // Set some prop values
    builder
      ..id = 'the_best_foo'
      ..color = '#ee2724';

    // Invoke as a function with the desired children
    // to return a new instance of the component.
    return builder('child1', 'child2');
  }

  /// Even better... do it inline! (a.k.a fluent)
  ReactElement baz() {
    return (Foo()
      ..id = 'the_best_foo'
      ..color = 'red'
    )(
      'child1',
      'child2'
    );
  }
}

See fluent-style component consumption for more examples on builder usage.

 

UiState

UiState is a Map class (just like UiProps) that adds statically-typed getters and setters for each React component state property in a class component.

mixin FooState on UiState {
  // ...
}

UiState is optional, and won’t be used for every component. Check out the UiStatefulComponent boilerplate for more information.

 

UiComponent2 (class-based components)

For guidance on updating to UiComponent2 from UiComponent, check out the UiComponent2 Migration Guide.

UiComponent2 is a subclass of react.Component2, containing lifecycle methods and rendering logic for components.

class FooComponent extends UiComponent2<FooProps> {
  // ...
}

 

uiFunction (function components)

uiFunction lets you declare a function component.

In JavaScript, function components are just plain functions, but in over_react this wrapper is needed to perform JS interop and wire up the typed props class.

mixin FooProps on UiProps {
  bool? isDisabled;
  List? items;
}

UiFactory<FooProps> Foo = uiFunction((props) {
  // Set default props using null-aware operators.
  final isDisabled = props.isDisabled ?? false;
  final items = props.items ?? [];

  // Return the rendered component contents here.
  return Fragment()(
    Dom.div()(items),
    (Dom.button()..disabled = isDisabled)('Click me!'),
  );
}, _$FooConfig); // The generated props config will match the factory name.

usageExample() => (Foo()..items = ['bar'])();

 

Accessing and manipulating props / state within UiComponent2

UiFactory<FooProps> Foo = castUiFactory(_$Foo); // ignore: undefined_identifier

mixin FooProps on UiProps {
  late String color;
  Function()? onDidActivate;
  Function()? onDidDeactivate;
}
mixin FooState on UiState {
  late bool isActive;
}

class FooComponent extends UiStatefulComponent2<FooProps, FooState> {
  @override
  Map get defaultProps => (newProps()
    ..color = '#66cc00'
  );

  @override
  Map get initialState => (newState()
    ..isActive = false
  );

  @override
  void componentDidUpdate(Map prevProps, Map prevState, [dynamic snapshot]) {
    var tPrevState = typedStateFactory(prevState);
    var tPrevProps = typedPropsFactory(prevProps);

    if (state.isActive && !tPrevState.isActive) {
      props.onDidActivate?.call();
    } else if (!state.isActive && tPrevState.isActive) {
      props.onDidDeactivate?.call();
    }
  }

  @override
  dynamic render() {
    return (Dom.div()
      ..modifyProps(addUnconsumedDomProps)
      ..style = {
        ...newStyleFromProps(props),
        'color': props.color,
        'fontWeight': state.isActive ? 'bold' : 'normal', 
      }
    )(
      (Dom.button()..onClick = _handleButtonClick)('Toggle'),
      props.children,
    );
  }

  void _handleButtonClick(SyntheticMouseEvent event) {
    setState(newState()
      ..isActive = !state.isActive
    );
  }
}

   

Fluent-style component consumption

The OverReact analyzer plugin has many lints and assists to make authoring OverReact components easier!

In OverReact, components are consumed by invoking a UiFactory to return a new UiProps builder, which is then modified and invoked to build a ReactElement.

This is done to make "fluent-style" component consumption possible, so that the OverReact consumer experience is very similar to the React JS / "vanilla" react-dart experience.

To demonstrate the similarities, the example below shows a render method for JS, JSX, react-dart, and over_react that will have the exact same HTML markup result.

   

DOM components and props

All react-dart DOM components (react.div, react.a, etc.) have a corresponding Dom method (Dom.div(), Dom.a(), etc.) in OverReact.

ReactElement renderLink() {
  return (Dom.a()
    ..id = 'home_link'
    ..href = '/home'
  )('Home');
}

ReactElement renderResizeHandle() {
  return (Dom.div()
    ..className = 'resize-handle'
    ..onMouseDown = _startDrag
  )();
}

   

Component Formatting

__A note on dart_style:__

Currently, dart_style (dartfmt) decreases the readability of components built using OverReact's fluent-style. See https://github.com/dart-lang/dart_style/issues/549 for more info.

We're exploring some different ideas to improve automated formatting, but for the time being, we do not recommend using dart_style with OverReact.

However, if you do choose to use dart_style, you can greatly improve its output by using trailing commas in children argument lists:

  • dart_style formatting:
    return (Button()
    ..id = 'flip'
    ..skin =
      ButtonSkin.vanilla)((Dom.span()
    ..className = 'flip-container')((Dom.span()..className = 'flipper')(
    (Dom.span()
      ..className =
          'front-side')((Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_RIGHT)()),
    (Dom.span()
      ..className =
          'back-side')((Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_LEFT)()))));
  • dart_style formatting, when trailing commas are used:
    return (Button()
    ..id = 'flip'
    ..skin = ButtonSkin.vanilla)(
    (Dom.span()..className = 'flip-container')(
    (Dom.span()..className = 'flipper')(
      (Dom.span()..className = 'front-side')(
        (Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_RIGHT)(),
      ),
      (Dom.span()..className = 'back-side')(
        (Icon()..glyph = IconGlyph.CHEVRON_DOUBLE_LEFT)(),
      ),
    ),
    ),
    );

Guidelines

To help ensure your OverReact code is readable and consistent, we've arrived at the following formatting rules.

Building custom components

Now that we’ve gone over how to use the over_react package in your project, the anatomy of a component and the DOM components that you get for free from OverReact, you're ready to start building your own custom React UI components.

  1. Start with one of the component boilerplate templates below (Or, use OverReact's code snippets for WebStorm/IntelliJ and VsCode).
  2. Fill in your props and rendering/lifecycle logic.
  3. Consume your component with the fluent interface.
  4. Run the app you’ve set up to consume over_react

    $ webdev serve

    That’s it! Code will be automatically generated on the fly by the builder!

Check out some custom component demos to get a feel for what’s possible!

 

Component Boilerplate Templates

 

Component Best Practices

 

 

 

Ignore Ungenerated Warnings Project-Wide

To avoid having to add // ignore: uri_has_not_been_generated to each component library on the part/import that references generated code, ignore this warning globally within analysis_options.yaml:

 analyzer:
   errors:
     uri_has_not_been_generated: ignore

Alternatively, include workiva_analysis_options which ignores this warning by default.

 

Contributing

Yes please! (Please read our contributor guidelines first)

   

Versioning

The over_react library adheres to Semantic Versioning: