A library for building statically-typed React UI components using Dart.
- Using it in your project
- Anatomy of an OverReact component
- Fluent-style component consumption
- DOM components and props
- Component Formatting
- Building custom components
- Contributing
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.
-
Add the
over_react
package as a dependency in yourpubspec.yaml
.dependencies: over_react: "^1.17.0"
-
Add the
over_react
transformer to yourpubspec.yaml
.transformers: - over_react # Reminder: dart2js should come after any other transformers that touch Dart code - $dart2js
Our transformer uses code generation to wire up the different pieces of your component declarations - and to create typed getters/setters for
props
andstate
. -
Include the native JavaScript
react
andreact_dom
libraries in your app’sindex.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="packages/react/react.js"></script> <script src="packages/react/react_dom.js"></script> <script type="application/dart" src="your_app_name.dart"></script> <script src="packages/browser/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-minifiedreact.js
/react_dom.js
files shown in the example above. -
Import the
over_react
library (and associated react libraries) intoyour_app_name.dart
, and initialize React within your Dart application. Then build a custom component and mount / render it into the HTML element you created in step 3.import 'dart:html'; import 'package:react/react.dart' as react; import 'package:react/react_dom.dart' as react_dom; import 'package:over_react/over_react.dart'; main() { // Initialize React within our Dart app setClientConfiguration(); // Mount / render your component. react_dom.render(Foo()(), querySelector('#react_mount_point')); }
-
Run
pub serve
in the root of your Dart project.
When running tests on code that uses our transformer (or any code that imports over_react
),
you must run your tests using Pub.
-
Add the
test/pub_serve
transformer to yourpubspec.yaml
after theover_react
transformer.transformers: - over_react - test/pub_serve: $include: test/**_test{.*,}.dart - $dart2js
-
Use the
--pub-serve
option when running your tests:$ pub run test --pub-serve=8081 test/your_test_file.dart
Note:
8081
is the default port used, but your project may use something different. Be sure to take note of the output when runningpub serve
to ensure you are using the correct port.
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 to our Pub transformer using an analogous annotation.
- UiFactory
- UiProps
- UiState (optional)
- UiComponent
UiFactory
is a function that returns a new instance of a
UiComponent
’s UiProps
class.
@Factory()
UiFactory<FooProps> Foo;
- This factory is the entry-point to consuming every OverReact component.
- The
UiProps
instance it returns can be used as a component builder, or as a typed view into an existing props map.
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.
@Props()
class FooProps extends UiProps {
// ...
}
@Factory()
UiFactory<FooProps> Foo;
@Props()
class FooProps extends UiProps {
String color;
}
@Component()
class FooComponent extends UiComponent<FooProps> {
// ...
}
void bar() {
FooProps props = Foo();
props.color = '#66cc00';
print(props.color); // #66cc00
print(props); // {FooProps.color: #66cc00}
}
/// You can 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
}
@Factory()
UiFactory<FooProps> Foo;
@Props()
class FooProps extends UiProps {
String color;
}
@Component()
class FooComponent extends UiComponent<FooProps> {
ReactElement bar() {
// Create a UiProps instance to serve as a builder
FooProps builder = Foo();
// Add props
builder.id = 'the_best_foo';
builder.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
is a Map class (just like UiProps
) that adds statically-typed getters and setters
for each React component state property.
@State()
class FooState extends UiState {
// ...
}
UiState is optional, and won’t be used for every component.
UiComponent
is a subclass of react.Component
, containing lifecycle methods
and rendering logic for components.
@Component()
class FooComponent extends UiComponent<FooProps> {
// ...
}
- This component provides statically-typed props via
UiProps
, as well as utilities for prop forwarding and CSS class merging. - The
UiStatefulComponent
flavor augmentsUiComponent
behavior with statically-typed state viaUiState
.
- Within the
UiComponent
class,props
andstate
are not justMap
s. They are instances ofUiProps
andUiState
, which means you don’t need String keys to access them! newProps()
andnewState()
are also exposed to conveniently create empty instances ofUiProps
andUiState
as needed.typedPropsFactory()
andtypedStateFactory()
are also exposed to conveniently create typedprops
/state
objects out of any provided backing map.
@Component()
class FooComponent extends UiStatefulComponent<FooProps, FooState> {
@override
getDefaultProps() => (newProps()
..color = '#66cc00'
);
@override
getInitialState() => (newState()
..isActive = false
);
@override
componentWillUpdate(Map newProps, Map newState) {
var tNewState = typedStateFactory(newState);
var tNewProps = typedPropsFactory(newProps);
var becameActive = !state.isActive && tNewState.isActive;
// Do something here!
}
@override
render() {
return (Dom.div()
..style = {
'color': props.color,
'fontWeight': state.isActive ? 'bold' : 'normal'
}
)(
(Dom.button()..onClick = _handleButtonClick)('Toggle'),
props.children
);
}
void _handleButtonClick(SyntheticMouseEvent event) {
_toggleActive();
}
void _toggleActive() {
setState(newState()
..isActive = !state.isActive
);
}
}
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.
-
React JS:
render() { return React.createElement('div', {className: 'container'}, React.createElement('h1', null, 'Click the button!'), React.createElement('button', { id: 'main_button', onClick: _handleClick }, 'Click me') ); }
-
React JS (JSX):
render() { return <div className="container"> <h1>Click the button!</h1> <button id="main_button" onClick={_handleClick} >Click me</button> </div>; }
-
Vanilla react-dart:
render() { return react.div({'className': 'container'}, react.h1({}, 'Click the button!'), react.button({ 'id': 'main_button', 'onClick': _handleClick }, 'Click me') ); }
-
OverReact:
render() { return (Dom.div()..className = 'container')( Dom.h1()('Click the button!'), (Dom.button() ..id = 'main_button' ..onClick = _handleClick )('Click me') ); }
Let’s break down the OverReact fluent-style shown above
render() { // Create a builder for a <div>, // add a CSS class name by cascading a typed setter, // and invoke the builder with the HTML DOM <h1> and <button> children. return (Dom.div()..className = 'container')( // Create a builder for an <h1> and invoke it with children. // No need for wrapping parentheses, since no props are added. Dom.h1()('Click the button!'), // Create a builder for a <button>, (Dom.button() // add a ubiquitous DOM prop exposed on all components, // which Dom.button() forwards to its rendered DOM, ..id = 'main_button' // add another prop, ..onClick = _handleClick // and finally invoke the builder with children. )('Click me') ); }
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
)();
}
- OverReact DOM components return a new
DomProps
builder, which can be used to render them via our fluent interface as shown in the examples above. DomProps
has statically-typed getters and setters for all "ubiquitous" HTML attribute props.- The
domProps()
function is also available to create a new typed Map or a typed view into an existing Map. Useful for manipulating DOM props and adding DOM props to components that don’t forward them directly.
- The
A note on dart_style:
Currently, dart_style (dartfmt) decreases the readability of components built using OverReact's fluent-style. See dart-lang/dart_style#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)(), ), ), ), );
To help ensure your OverReact code is readable and consistent, we've arrived at the following formatting rules.
-
ALWAYS place the closing builder parent on a new line.
Good:
(Button() ..skin = ButtonSkin.SUCCESS ..isDisabled = true )('Submit')
Bad:
(Button() ..skin = ButtonSkin.SUCCESS ..isDisabled = true)('Submit')
-
ALWAYS pass component children on a new line with trailing commas and 2 space indentation.
Good:
Dom.div()( Dom.span()('nested component'), )
Dom.div()( Dom.span()('nested component A'), Dom.span()('nested component B'), )
Bad:
// Children are not on a new line; in most cases, // this makes it difficult to quickly determine nesting. Dom.div()(Dom.span()('nested component'), Dom.span()('nested component'))
// With nested hierarchies, continuation indents can quickly result // in a "pyramid of Doom" Dom.div()( Dom.ul()( Dom.li()( Dom.a()('A link!') ) ) )
// Omitting trailing commas makes it a pain to rearrange lines Dom.div()( Dom.span()('nested component A'), Dom.span()('nested component B') ) Dom.div()( Dom.span()('nested component B') // ugh, need to add a comma here... Dom.span()('nested component A'), )
-
AVOID passing children within lists; lists should only be used when the number/order of the children are dynamic.
Good:
Dom.div()( Dom.span()('nested component'), Dom.span()('nested component'), )
var children = [ Dom.div()('List of Items:'), ]..addAll(props.items.map(renderItem)); return Dom.div()(children)
Bad:
Dom.div()([ (Dom.span()..key = 'span1')('nested component'), (Dom.span()..key = 'span2')('nested component'), ])
-
AVOID specifying more than one cascading prop setter on the same line.
Good:
(Dom.div() ..id = 'my_div' ..className = 'my-class' )()
Bad:
(Dom.div()..id = 'my_div'..className = 'my-class')()
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.
- Start with one of the component boilerplate templates below.
- Component (props only)
- Stateful Component (props + state)
- Flux Component (props + store + actions)
- Stateful Flux Component (props + state + store + actions)
-
Fill in your props and rendering/lifecycle logic.
-
Consume your component with the fluent interface.
-
Run the app you’ve set up to consume
over_react
$ pub serve
That’s it! Code will be automatically generated on the fly by Pub!
Check out some custom component demos to get a feel for what’s possible!
-
import 'dart:html'; import 'package:react/react.dart' as react; import 'package:react/react_dom.dart' as react_dom; import 'package:react/react_client.dart'; import 'package:over_react/over_react.dart'; @Factory() UiFactory<FooProps> Foo; @Props() class FooProps extends UiProps { // Props go here, declared as fields: bool isDisabled; Iterable<String> items; } @Component() class FooComponent extends UiComponent<FooProps> { @override Map getDefaultProps() => (newProps() // Cascade default props here ..isDisabled = false ..items = [] ); @override render() { // Return the rendered component contents here. // The `props` variable is typed; no need for string keys! } }
-
import 'dart:html'; import 'package:react/react.dart' as react; import 'package:react/react_dom.dart' as react_dom; import 'package:react/react_client.dart'; import 'package:over_react/over_react.dart'; @Factory() UiFactory<BarProps> Bar; @Props() class BarProps extends UiProps { // Props go here, declared as fields: bool isDisabled; Iterable<String> items; } @State() class BarState extends UiState { // State goes here, declared as fields: bool isShown; } @Component() class BarComponent extends UiStatefulComponent<BarProps, BarState> { @override Map getDefaultProps() => (newProps() // Cascade default props here ..isDisabled = false ..items = [] ); @override Map getInitialState() => (newState() // Cascade initial state here ..isShown = true ); @override render() { // Return the rendered component contents here. // The `props` variable is typed; no need for string keys! } }
-
import 'dart:html'; import 'package:react/react.dart' as react; import 'package:react/react_dom.dart' as react_dom; import 'package:react/react_client.dart'; import 'package:over_react/over_react.dart'; @Factory() UiFactory<BazProps> Baz; @Props() class BazProps extends FluxUiProps<BazActions, BazStore> { // Props go here, declared as fields. // `actions` and `store` are already defined for you! } @Component() class BazComponent extends FluxUiComponent<BazProps> { getDefaultProps() => (newProps() // Cascade default props here ); @override render() { // Return the rendered component contents here. // The `props` variables is typed; no need for string keys! // E.g., `props.actions`, `props.store`. } }
-
import 'dart:html'; import 'package:react/react.dart' as react; import 'package:react/react_dom.dart' as react_dom; import 'package:react/react_client.dart'; import 'package:over_react/over_react.dart'; @Factory() UiFactory<BazProps> Baz; @Props() class BazProps extends FluxUiProps<BazActions, BazStore> { // Props go here, declared as fields. // `actions` and `store` are already defined for you! } @State() class BazState extends UiState { // State goes here, declared as fields. } @Component() class BazComponent extends FluxUiStatefulComponent<BazProps, BazState> { getDefaultProps() => (newProps() // Cascade default props here ); @override Map getInitialState() => (newState() // Cascade initial state here ); @override render() { // Return the rendered component contents here. // The `props` variables is typed; no need for string keys! // E.g., `props.actions`, `props.store`. } }
-
ALWAYS write informative comments for your component factories. Include what the component relates to, relies on, or if it extends another component.
Good:
/// Use the `DropdownButton` component to render a button /// that controls the visibility of a child [DropdownMenu]. /// /// * Related to [Button]. /// * Extends [DropdownTrigger]. /// * Similar to [SplitButton]. /// /// See: <https://link-to-any-relevant-documentation>. @Factory() UiFactory<DropdownButtonProps> DropdownButton;
Bad:
/// Component Factory for a dropdown button component. @Factory() UiFactory<DropdownButtonProps> DropdownButton;
-
ALWAYS set a default / initial value for
props
/state
fields, and document that value in a comment.Why? Without default prop values for bool fields, they could be
null
- which is extremely confusing and can lead to a lot of unnecessary null-checking in your business logic.Good:
@Props() DropdownButtonProps extends UiProps { /// Whether the [DropdownButton] appears disabled. /// /// Default: `false` bool isDisabled; /// Whether the [DropdownButton]'s child [DropdownMenu] is open /// when the component is first mounted. /// /// Determines the initial value of [DropdownButtonState.isOpen]. /// /// Default: `false` bool initiallyOpen; } @State() DropdownButtonState extends UiState { /// Whether the [DropdownButton]'s child [DropdownMenu] is open. /// /// Initial: [DropdownButtonProps.initiallyOpen] bool isOpen; } @Component() DropdownButtonComponent extends UiStatefulComponent<DropdownButtonProps, DropdownButtonState> { @override Map getDefaultProps() => (newProps() ..isDisabled = false ..initiallyOpen = false ); @override Map getInitialState() => (newState() ..isOpen = props.initiallyOpen ); }
Bad:
@Props() DropdownButtonProps extends UiProps { bool isDisabled; bool initiallyOpen; } @State() DropdownButtonState extends UiState { bool isOpen; } @Component() DropdownButtonComponent extends UiStatefulComponent<DropdownButtonProps, DropdownButtonState> { // Confusing stuff is gonna happen in here with // bool props that could be null. }
-
AVOID adding
props
orstate
fields that don't have an informative comment.Good:
@Props() DropdownButtonProps extends UiProps { /// Whether the [DropdownButton] appears disabled. /// /// Default: `false` bool isDisabled; /// Whether the [DropdownButton]'s child [DropdownMenu] is open /// when the component is first mounted. /// /// Determines the initial value of [DropdownButtonState.isOpen]. /// /// Default: `false` bool initiallyOpen; } @State() DropdownButtonState extends UiState { /// Whether the [DropdownButton]'s child [DropdownMenu] is open. /// /// Initial: [DropdownButtonProps.initiallyOpen] bool isOpen; }
Bad:
@Props() DropdownButtonProps extends UiProps { bool isDisabled; bool initiallyOpen; } @State() DropdownButtonState extends UiState { bool isOpen; }
Below you’ll find some common errors / issues that new consumers run into when building custom components.
Don’t see the issue you're having? Tell us about it.
ⓧ Exception: The null object does not have a method 'call'.
This error is thrown when you call a @Factory()
function that has not been initialized due to
the over_react
transformer not running, you’ll get this error.
Make sure you’ve followed the setup instructions.
ⓧ GET http://localhost:8080/src/your_component.dart
ⓧ An error occurred loading file: http://localhost:8080/src/your_component.dart
When the over_react
transformer finds something wrong with your file, it logs an error in Pub and causes the
invalid file to 404. This ensures that when the transformer breaks, pub build
will break, and you’ll know about it.
Check your pub serve
output for errors.
Yes please! (Please read our contributor guidelines first)
The over_react
library adheres to Semantic Versioning:
- Any API changes that are not backwards compatible will bump the major version (and reset the minor / patch).
- Any new functionality that is added in a backwards-compatible manner will bump the minor version (and reset the patch).
- Any backwards-compatible bug fixes that are added will bump the patch version.