aappddeevv / scalajs-reaction   1.0.0-RC1

MIT License Website GitHub

scala.js react bindings in the spirit of ReasonReact

Scala versions: 3.x
Scala.js versions: 1.x

Use react hooks and scala.js to catch the best user experience.

scalajs-reaction: scalajs-reaction, scala: scala

scalajs reaction focuses on scala3 and interop with the react js world.

Use react version 18+ and/or the experimental to ensure that all hooks defined in this library are included in the underlying js source. Use the latest react-native.

Get started with the docs

Here's how easy it is to declare a function component.

val HelloWorld: ReactFC0 = () => div("hello world")

What is scalajs-reaction?

An interop library with reactjs and the react ecosystem.

This library is small and focuses on simple js function components and hooks. Hooks are described on the react page.

scalajs-reaction emphasizes:

  • Easy to learn/use. Your component is just a standard scala.js js function.

  • Integration into an existing project by making it easy to import/export components. You just export the function using standard scala.js.

  • Easy to fit into existing application including those using global state-managed solutions such as redux.

  • Build your own abstractions on top e.g. things like slinky or scalajs-react.

scalajs-reaction allows you to build your entire interface in scalajs-reaction. As long as your front-end solution can manage the model of compiler scala.js's output, you should consider scalajs-reaction for your solution. By providing a thin veneer over standard scala functions and hooks, it eschews abstractions and avoids getting in your way.

The react-native use-case for scala.js may be more compelling than for web applications. Support is provided for the experimental, reactjs concurrent API.

A g8 template is available. Use sbt new aappddeevv/scalajs-reaction-app.g8 (in transition to scala3) to create a new project.

Creating Components

It's easy to create a component and render it:

// hello world component
val HelloWorld: ReactFC0 = () => div("hello world")
// ....
react_dom.createAndRenderWithId(HelloWorld, "container")

ReactFC0 does not do much other than ensure that the scala function on the right becomes a js function--which is all that is needed to use react. You could have just declared it directly:

val HelloWorld: js.Function0[ReactNode] = _ => div("hello world")

If you need to pass in an argument, remember that react function components requires a single js object parameter.

object MyComponent {
    trait Props extends js.Object:
        val name: String

    // I use this declaration style alot.
    val render: ReactFC[Props] = props =>
      div("hello " + props.name)

    // optional but helpful with browser debugging tools
    render.displayName = "MyComponent"

    def apply(props: Props) = createElement(render, props)
}

Using it is equally simple:

val myNodeToUse = MyComponent(new { name = "foo" })

ReactFC says that the function component HelloWorld takes a single parameter, of type Props. You do not need to use ReactFC, you could just use standard scala and standard react:

object MyComponent {
    trait Props ...

    val render: js.Function1[Props, ReactNode] = props => div(s"hello ${props.name}")

    def apply(props: Props) = createElement(render, props)
}

or with types and extension methods:

object MyComponent {
  trait Props ...

  // A standard scala function is a val, but its better to type it as ReactFC[Props]
  val render: Props => ReactNode = props => div(s"hello ${props.name}")

  // A standard def scala function, it's better to use a val
  //def render(props: Props): ReactNode = div(s"hello ${props.name}")

  // ReactElementTuple causes scala=>js function conversion using standard scala methods
  def apply(props: Props): ReactElementTuple = (render, props)

  // Or convert using the `.elementWith` extension method which reads nicely
  // This is what I usually use.
  def apply(props: Props) = render.elementWith(props)
}

That's how simple this facade is. You use the easy parts of scala.

To create a higher-order component or a "container/pure renderer" design, use function composition:

def withPageLogging[P <: js.Object](component: ReactFC[P]): ReactFC[P] = props => {
  useEffectMounting(() => println("LOG PAGE!"))
  createElement(component, props) // or component.elementWith(props)
}

React needs to control when the props are applied to the rendering function, not you. A react "element" is really just a data structure that is a description of the element's rendering logic and props. The actual rendered element is created under react's control, not yours. There are other component specification patterns you can use as well or you can easily create your own like the above. See the docs.

If you want to ensure your component only renders when the props change, use memo(). memo uses shallow compare checking to see if the props have changed. An extension method is available for memo to apply it as syntax. You need to take into account js's notion of equality when using function components and hooks or provider your comparator, something useful when you have scala objects in your props.

Add state and other effects to your component using react hooks just like you normally would.

The scala.js facade implementation is ~50 lines of code and is easily maintained. Various conversion and extension methods are provided to make the API ergonomic.

You have many choices on how to create your components and they are all straightforward. Depending on the component library you use, having choices helps you find the easiest way to access the components in your application. This library does not force many conventions on your code.

This facade uses standard scala.js concepts. Higher level components, e.g. composition of smaller components, can use more idiomatic scala constructs. This library does not fight scala or scala.js to make it more like js. It uses the tools provided by scala and scala.js to make it easier to write react applications and control react rendering optimizations.

What is react function component?

react function components are not pure functions and can never be pure functions. Think of them more like a "context" that is established for some part of the DOM that also returns HTML builder instructions each time it is called. If you come from the java Spring world, think of it as a bean factory that knows how to emit render instructions. It is better to think of react function components as an object and not as a function.

Usage

The libraries are all at the organization org.ttgoss.js and available on maven.

Include the library in your build:

val scalaJsReactVersion = "latest.version" // see the badge above

// grab the the latest version or use a specific version
libraryDependencies ++= Seq(
    "org.ttgoss.js" %%% "react" % scalaJsReactVersion,
    "org.ttgoss.js" %%% "vdom" % scalaJsReactVersion,

    // optionals
    // if you need react-dom
    "org.ttgoss.js" %%% "react-dom" % scalaJsReactVersion

    // Microsoft fabric UI components, "MS office", css-in-"scala"
    // css-in-scala can be used independently of fabric
    "org.ttgoss.js" %%% "fabric" % scalaJsReactVersion,

    // Material UI components (bootstrap is also available but limited)
    "org.ttgoss.js" %%% "mui" % scalaJsReactVersion,

    // if you integrate with redux
    "org.ttgoss.js" %%% "redux" % scalaJsReactVersion,

    // if you need prop-types--you only need these for interop scenarios
    "org.ttgoss.js" %%% "prop-types" % scalaJsReactVersion,

    // if you need react-native
    "org.ttgoss.js" %%% "native" % scalaJsReactversion)
  )
  // ...
//
// Add import scala.language.implicitConversions to each file or
// add scalacOptions += Seq("-language:_") to your settings
// to enable implicit conversions and all other language features

// I use explicit nulls and you should to in scala.js applications at least
// for the time being.
  scalacOptions += Seq("-Yexplicit-nulls")

Include JS libraries

Do not forget to include the react libraries in your execution environment. For react 16+, the libraries have been split out into multiple libraries. For the reactjs based modules, the javascript dependencies are:

  • core: react
  • react-dom: react-dom
npm i --save react
npm i --save react-dom

React 16.8+, the one with hooks, is required. Some experimental APIs are supported so you may want to use the latest experimental release.

What's available

There are many bindings available as most of the focus has been on hand-crafted bindings in order to improve ergonomics. We'll work with ScalablyType to improve the availability of bindings, however, they are quite simple to write using scalajs-reaction. All of these libraries use the group name org.ttgoss.js:

  • apollo (apollo3 only)
  • bootstrap
  • dataloader
  • data-validation (applicative data validation)
  • express
  • fabric (office-ui-fabric-react): Custom and ergonomic facade.
  • fabric-experiments
  • forms: Advanced, all-scala.js forms package.
    • In transition so its not currently available.
  • formik
  • handlebars
  • helmet
  • jshelpers: Helpers for working with js data. Includes full js.Promise extension methods to avoid Future.
  • jss
  • lodash
  • loglevel
  • luxon (datetime library, evolved from moment)
  • msal
  • mssql
  • mui
  • native
  • pathtoregexp
  • plotlyjs
  • prop-types
  • react
  • react-content-loader
  • react-device-detect
  • react-dom
  • react-big-calendar
  • react-fast-compare
  • react-native-nativebase
  • react-native-elements
  • react-navigation
  • react-native-sideswipe
  • react-plotlyjs
  • react-redux
  • react-responsive
  • react-helmet
  • react-flexbox-grid
  • react-router-dom 6 (the v5 bindings are no longer available)
  • recoil (facebook state management)
  • use-query-params
  • use-deep-compare-effect
  • vdom
  • whydidyourender (use include/exclude regexs, see the readme in that directory)

Some of the external libs have just enough scala.js bindings to write an app with are not fully fleshed out. In most cases, there are enhancements to make using the library easier.

Most of the packages use a simple namespace hierarchy starting with the package name versus "ttg" or "scalajs-reaction" or anything complicated. You are likely only to use one set of react libraries per application so you should not encounter any package namespace collisions.

In many cases, the full package label has been shortened to make it easier to import the content. The package names closely mirror the javascript libraries themselves.

You will most likely also need DOM bindings. Here's a link to the api for org.scala-js scalajs-dom bindings: javadoc.

jshelpers

Package jshelpers is available to help with strange types. Review the docs for extra syntaxt that you can use.

Generally, if you model your data such as js.UndefOr[A|Null] then you will want to turn on -Xexplicit-nulls so that the Null type becomes explicit and not inferred.

Documentation

Client:

Integrated API documentation:

Sometimes the documentation generation process does not work so if you need docs it is probably best to generate them locally.

Generate integrated documents using unidoc then open the toplevel page:

get clone https://github.com/aappdddeevv/scalajs-reaction
sbt "docs/unidoc"
# linux has xdg-open to open a file from the command line
xdg-open website/scalajs-reaction/static/api/index.html

Demo

You can start the demo locally if you run sbt npmRunDemo. A browser page should open automatically. If not, point your browser to https://localhost:8080. You can also just load the demo files (after its built) into your browser but note that the icons will not render correctly since they require a fetch.

Argh!!! I'm having trouble with gh-pages so the demo probably does not work at htis time. Run it locally after you clone the repo.

Motivation

I was looking for a react facade that rethought reactive interactions. Many of the existing facades are straight adaptions of the standard react library and make it quite easy to program react in a scala environment.

The ReasonReact facade, using a modified OCaml syntax, keeps it quite simple. This library is actually quite small and simple and relies on functions. Scala.js easily supports creating classes that map into javascript classes and hence into react component classes. However, with parallel fiber coming to react and other enhancements that are planned, we need to withstand the change in the core react infrastructure and use an approach that is more sustainable in the long run. scala.js js functions are about as simple as you can get.

Suitability

Scala.js requires a bundling model that leverages scala.js technology smartly especially when it comes to bundling/splitting and bundle size.

For mission critical apps please ensure you have explored the impact of using scala.js on your application. In many cases, it may not matter.

The core scala.js infrastructure costs you about 2.5k and increases as you use features such as immutability, the collections library or, of course, add data structures to your code. Eventually, scala.js will be free fo the collections library and other scala'ish dependencies and it's minimum file size will shrink further.

Related

There are a few scala.js react facades/implementations available:

The facades differ in their facade approach but many recreate all of the API parts found in reactjs. japgolly works hard to put a more safe, functional API in place from the outset. Some of these facades have alot of complex facade components to help deal with the type and side-effects flexibility found in react.

In theory, all of these can "export" a scala.js component to be used in javascript environments. However, various libraries that go deep into scala-land means that they are harder to use from javascript land often because they wrap the javascript props and state objects in a way that makes them less usable from the javascript world. Most, if not all, libraries allow you to use javascript defined components fairly directly via some type of adoption or import process.

Libraries you may be interested in:

  • binding.scala: reactive UI framework. It uses a new binding framework for scala. Works on jvm and js. The binding framework is at a individual element level like mobx. You can use xml syntax as well. The binding approach is called "precise" binding.
  • diode: redux replacement.
  • laminar: a reactive web framework.
  • levsha: Fast pure scala.js virtual dom.
  • pine: XML/HTML builder.
  • scalacss: A solid css-in-scala solution.
  • scala-dom-types: for dom attributes.
  • scala-tags: XML/HTML builder.
  • outwatch: reactive UI framework.
  • udash is another reactive framework that is not react based, but reactive.
  • udash-css: A css-in-scala framework.

License

MIT license. GitHub license

Copyright 2018 The Trapelo Group LLC.

The logo was created partially from content provided by Scott De Jonge under CC3.0.