A library for building statically-typed React UI components using Dart.
Dart 2 Migration Guide
If you have existing over_react code written on Dart 1 and want to upgrade to Dart 2, please read the Dart 2 Migration Guide
- 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: ^2.0.0
-
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/javascript" defer src="your_app_entrypoint.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
andreact_dom
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.Be sure to namespace the
react_dom.dart
import asreact_dom
to avoid collisions withUiComponent.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'; main() { // Initialize React within our Dart app setClientConfiguration(); // Mount / render your component. react_dom.render(Foo()(), querySelector('#react_mount_point')); }
-
Run
pub run build_runner serve
in the root of your Dart project.
Note: After running a build, you'll have to restart your analysis server in your IDE for the built types to resolve properly. Unfortunately, this is a known limitation in the analysis server at this time. See: dart-lang/sdk#34344
When running tests on code that uses our builder (or any code that imports over_react
),
you must run your tests using build_runner.
Warning: Do not run tests via
pub run build_runner test
in a package while another instance ofbuild_runner
(e.g.pub run build_runner serve
)is running in that same package. This workflow is unsupported by build_runner
-
Run tests through build_runner, and specify the platform to be a browser platform. Example:
$ pub run build_runner test -- -p chrome test/your_test_file.dart
-
When running tests in
over_react
, ourdart_test.yaml
specifies some handy presets for running tests in DDC and dart2js:Note: These presets exist only in
over_react
.- To run tests in
over_react
compiled via DDC, run:
$ pub run build_runner test -- -P dartdevc
- To run tests in
over_react
compiled via dart2js, run:
$ pub run build_runner test -r -- -P dart2js
- To run tests in
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 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 = _$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 {
// ...
}
- Note: The builder will make the concrete getters and setters available in a generated class which has the same name
as the class annotated with
@Props()
, but without the_$
prefix (which would beFooProps
in the above code). The generated class will also have the same API. So, consumers who wish to extend the functionality of_$FooProps
should extend the generated version,FooProps
.
@Factory()
UiFactory<FooProps> Foo = _$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 = _$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.
- Note: The builder will make the concrete getters and setters available in a generated class which has the same name
as the class annotated with
@State()
, but without the_$
prefix (which would beFooState
in the above code). The generated class will also have the same API. So, consumers who wish to extend the functionality of_$FooState
should use the generated version,FooState
.
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 (Or, use OverReact's code snippets for Intellij and Vs Code).
- 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 run build_runner 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!
-
import 'package:over_react/over_react.dart'; @Factory() UiFactory<FooProps> Foo = _$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:over_react/over_react.dart'; @Factory() UiFactory<BarProps> Bar = _$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:over_react/over_react.dart'; @Factory() UiFactory<BazProps> Baz = _$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:over_react/over_react.dart'; @Factory() UiFactory<BazProps> Baz = _$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 = _$DropdownButton;
Bad:
/// Component Factory for a dropdown button component. @Factory() UiFactory<DropdownButtonProps> DropdownButton = _$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; }
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.