Permalink
Fetching contributors…
Cannot retrieve contributors at this time
465 lines (371 sloc) 23.5 KB

1.0.0

This describes the changes between v0.11.3 and v1.0.0. Notes for the 1.0 release canditates are here.

This is one big document. Everything's changed. Grab a coffee.

Background

scalajs-react was originally a proof-of-concept, experimenting both with React and Scala.JS back in 2014. Once the concept was validated it immediately began being used in anger in a real-world project, and over the years this drove the discovery of additional required changes and features. These changes were usually implemented via hacks due a lack of initial design and discovered direction. Oft times how to solve certain problems was unclear and begat more experimentation which created more churn and mess. To be honest, I was also less experienced in Scala than I am today, and wrote code in a way that sometimes defied readability and easy comprehension when in a team: implicits everywhere, symbols and abbreviations all over the place (Haskell influence), etc.

In the end, the v0.x branch ended up being a codebase too design-constrained and near impossible to extend, hacks everywhere, littered with experiments both successful and failed.

It was time to give this library a proper, solid foundation. This time around, there was plenty of 20/20 hindsight and real-world project code to draw on to guide tough decisions. It still doesn't have everything imaginable, but it is infinitely more extensible now, meaning it will be much easier where difficult, and possible where impossible, to add more functionality to this. It's even possible for people to start making external modules for their features if they want to, as the library is finally open enough to support it.

I think it's ended up in a really good place. I hope you enjoy the new experience.

High-Level Summary

The vast majority of the core module has been rewritten from scratch. The other modules have received major revision too and in some cases, rewriting.

Simplification

Both the library internals and the code you write as a library user are much simpler now. Complex, verbose types have been simplified (no more N <: TopNode everywhere). Changes are simple to make and don't require hacks anymore. Comprehending and maintaining existing functionality is easy now.

An effort has also been made to make the experience consistent. Different parts of the library that do related things, now have consistent names and a consistent API. This is more cohesive, less surprising and ultimately less to remember.

Reduction

Contributing to Simplification, is Reduction. Similar concepts, structures, and utilities have been consolidated. Experiments that were too noisy and not useful enough have been removed. So too have little utilities that didn't provide enough benefit.

There's now less to learn, less to remember, less caveats to consider, and a consistent way of doing things.

Readability

A common, very valid complaint was that it had poor readability. There were too many symbols, inconsistencies in the API, single-letter suffixes all over the place, non-obvious abbreviations. (Example: Px.bs($).propsM)

All (ok, 98%) of this has been removed and replaced with a clearer API. In some places it's more verbose now but really, especially with auto-complete, what's an extra 10 chars on a line, every now and then? The readability improvement is worth it. Less cognitive load.

Transparency

As everything previously grew from JS facades, there was a lot of implicits and monkey-patching. No longer. Nothing in components is hidden anymore, types provide full access to their lineage and you can see exactly how the layers stack. For example it's completely transparent that (and how) a ScalaComponent builds on a JsComponent which in turn has a raw JS-world value.

Interoperability

It is now easy-peasy for scalajs-react code to

  • use React components written in other languages
  • be used from other languages
  • interoperate with other Scala.JS React libraries

There are two main reasons for this.

  1. There is now an underlying, generic representation of components. Language-agnostic. Scala components (created with ScalaComponent.builder, previously ReactComponentB) are no longer a special case and have no special privileges in the library. One can now choose to ignore the builder completely and use another mechanism to create their components.
  2. The Transparency changes noted above. As it's trivial to both get at raw (underlying) JS values and see the transformations on top of them now, it will be straightforward to use raw JS values as the bridge and just extract or wrap as necessary.

Significant Changes

The core module

  • Generic representations. There is now a generic interface for components and their unmounted & mounted states. This allows any kind of component (JS, Clojure, ES6 classes, some custom super-simple Scala wizard) to be used and integrate with the library. Many library features now work with and provide functionality to all kinds of components. Want Scalaz extensions to some Typescript-based component you found and are using in Scala? done! Want access to a parent component's state regardless of what kind of component it is, regardless of whether the state you're interested in is a subset or whole, has transformation or not? done!

  • Proper representation of non-Scala components. Various types of components, and their idiosyncrasies, can now be modelled easily. Included in the library are four types:

    • JsComponent - It's now a one-liner (not counting Scala.JS facades) to use community components written in pure JS. No more hacks!

      @JSName("Blah")                // If you're not using modules
      @JSImport("./blah.js", "Blah") // If you're using modules
      @js.native
      object BlahJs extends js.Object
      
      val Blah = JsComponent[JsProps, Children.None, JsState](BlahJs)

      js.Dynamic also works:

      val Blah = JsComponent[JsProps, Children.None, JsState](js.Dynamic.global.Blah)

      You can even get type-safety for JS components that expose ad-hoc methods once mounted; just specify the JS facade.

      val Blah = JsComponent[JsProps, Children.None, JsState](BlahJs).addFacade[BlahFacade]
    • JsFnComponent - React "functional components" written in pure JS. Also a one-liner to create, similar to above.

    • ScalaComponent - Create React components in Scala using the functional builder with an optional backend. This is the best way to write components and is still around. Where as there used to be ReactComponentB[Props]… before, it's now ScalaComponent.builder[Props]….
      There's also ScalaComponent.static for creating efficient, static views.

    • ScalaFnComponent - Create React "functional components" in Scala. Basically a JsFnComponent built on a Scala function, compatible with Scala types.

  • Transformation of ALL component type parameters. When you have a component (or its Unmounted or Mounted stages), you can now completely map everything that goes in and out of it. Specifically you can:

    • Transform props types and values (at top-level, and in Unmounted & Mounted too).
    • Transform state types and values (at top-level, and in Unmounted & Mounted too).
    • Transform the constructor.
    • Transform the Unmounted representation.
    • Transform the Mounted representation.
    • Change the effect type once Mounted.
  • Transparency. No more hidden magic.

    • All JS facades are in a separate .raw package (without any Scala niceness).
    • All components expose their underlying JS values. Call .raw on anything to get what you'd have if you were using pure JS.
    • JS-based components have a Scala representation that allows nice, safe usage. It's transparent that you've got a Scala facade over the raw JS.
    • The structure of components that you create using the Scala Builder are completely transparent too. You have access to the underlying JsComponent (showing exactly how the Scala boxing and backend support is implemented), and access to its raw JS value.
    • Nearly no more implicits are used. Nearly all the methods are now directly on the objects.
  • Interoperability. It should be trivial to mix scalajs-react with other React libraries, regardless of the language.

    • Using scalajs-react from JS or another language: due to the transparency changes you have full access to all underlying JS values and any transformations required.
    • Using scalajs-react from another Scala-based React library: see above point, one could create implicit conversions as necessary.
    • Using JS from scalajs-react: one-liner; see above.
    • Using another language or Scala-based library from scalajs-react: either grab the pure JS values and call a few methods to add any required transformations, or create your own representations by extending the generic traits in the library.
  • Proper constructors. More accuracy, safety, flexibility, extensibility.

    • Children are now finally declared. There are currently two declarations: Children.None and Children.Varargs. This means that you can no longer pass children to a component that won't use them, and you can no longer forget to pass children to a component that uses them. It might be an idea to add another case for components that want exactly one child, and enforce it in the constructor but this hasn't been done yet. The code is open to it which is nice.
    • Props values can be pre-specified. This happens automatically when creating components with singleton types like Unit and Null. It's also customisable in various ways.
    • Input and output can be transformed.
    • Additional fields on the raw/underlying JS props object can be specified.
    • Example usage:
      UserProfileComponent(userId)                // Component with props and Children.None
      BorderComponent("red")(h1("Hi!"), p("bye")) // Component with props and Children.Varargs
      RedBorderComponent(h1("Hi!"), p("bye"))     // Component with preset props and Children.Varargs
      DateTimeComponent()                         // Component with preset props and Children.None
  • Virtual DOM major revision

    • Under-the-hood, the types have been rewritten and simplified, and is now easier to work and maintain. This no longer bears any resemblance to Scalatags. Scalatags was however, tremendously helpful in this journey so if you have a chance, please join me in giving @lihaoyi a big thanks for his work.

    • VDOM attributes now have type-safety with regard to their values. Eg button(disabled := 123) no longer compiles because the library knows that the disabled attribute requires a boolean; button(disabled := true) works.

    • Event-handler attributes now know which types of events they accept. Eg onMouseDown knows to expect a mouse event and won't compile if you pass it a drag event handler.

    • No more automatic expansion of Seqs. Either add .toTagMod or flatten it out of an array, or use .toVdomArray to turn it into an array from React's perspective (which requires keys).

    • Optional VDOM supported when enclosed in Option, js.UndefOr, or scalaz.Maybe.

    • All VDOM now has .when(condition) and .unless(condition), allowing you to conditionally include/omit dom. This replaces the cond ?= (vdom) syntax.

      Note, that evaluation semantics have changed: vdom was a "by name" parameter in cond ?= (vdom), providing lazy semantics. When using .when(condition) and .unless(condition), vdom is evaluated, regardless of the condition value.

    • React node array handling is safer, more efficient and has its own type (VdomArray) with a nicer interface.

    • Manually-specified style objects now compose with other style attributes.

    • Improved efficiency for VDOM representation and creation.

    • Smaller JS output size.

    • All VDOM is now composable via .apply. This was usually the case but there were a few corner cases that had differences. When it comes to TagMod, the + operator has been removed -- to join many at once, instead of a + b + c + …, use TagMod(a, b, c, …) or the less efficient a(b)(c)(…).

    • There's now a VDOM cheatsheet.

  • Refs revision

    • React.JS has deprecated and will eventually remove String-based refs, and so they have been removed from scalajs-react.
    • New type-safe refs to HTML/SVG tags that preserve the DOM type.
    • New type-safe refs to JS components.
    • New type-safe refs to Scala components.
    • Functional components do not support refs and so, there is no mechanism to do so anymore. In scalajs-react v0.x you could create refs to anything so this is an improvement.
  • PropsChildren now has its own type that provides simple, idiomatic Scala usage.

  • Name revision

    There are many types and methods that remain from v0.x and have been renamed for clarity and readability. Gone are the abbreviations, the symbols like $ and _, the one letter suffixes, etc. Not all cases, but the vast majority. The result is code can occasionally be a little bit more verbose but much clearer. And don't worry about having to tediously update your codebase; there is a migration script will take of it for you.

  • Simple types for various purposes

    The underlying types used by components are complex. It's necessary to support all the desirable features we now have. What's not necessary is for that to leak into users' code. There are now many simple ways of accessing and/or abstracting over components. Which one you choose depends on the use-case and there is a guide in TYPES.md.

    Also: components no longer track DOM types. In cases where accessing a component's DOM is desirable, add a method to the backend (or externally) that casts it. Example:

    import org.scalajs.dom.html
    import japgolly.scalajs.react._
    
    object MyComponent {
      // ...
    
      class Backend(bs: BackendScope[Props, Unit]) {
    
        // Cast it yourself when desired
        def getDom: CallbackTo[html.TextArea] =
          bs.getDOMNode.map(_.domCast[html.TextArea])
      }
    }
  • ES6 classes

    Components created with ScalaComponent.builder now result in ES6 classes under-the-hood, instead using React.createClass which was deprecated in React v15.5. There is also a raw facade for ES6 classes, and external React classes integrate with scalajs-react like any other components.

The extra module

  • Reduction: ExternalVar and ReusableVar have been consolidated into a single class called StateSnapshot. The advantages are:

    • Reusability is now optional. Component clients don't have to jump through hoops if they don't care about reusability.
    • No more having to change entire call graphs when adding/removing reusability (as was the case when deciding to switch ExternalVar for ReusableVar or vica-versa.)
    • Users have one type to learn, and no longer have to learn subtleties between two very similar types.
  • The StateSnapshot (previously ExternalVar and ReusableVar) object now has a nice construction mini-DSL and even supports zooming.

  • Reduction: ReusableFn, ReusabaleVal, and ReusabaleVal2 have all been consolidated into a single class called Reusable. This means we now simply have Reusability[A] and Reusable[A], the difference being that Reusability[A] is for ∀a∈A, and Reusable[A] is for ∃a∈A. In other words, Reusability[A] provides reusability for type A and all its values, and Reusable[A] is a single value of A that is reusable.
    Reusable functions ReusableFn[A, B] are now Reusable[A => B]. (The A ~=> B alias now reflects this so you won't need any change where the alias is used.) Instead of creating them via ReusableFn(…), it's now Reusable.fn(…).

The test module

  • Similar to the changes in core, there is a package japgolly.scalajs.react.test.raw which contains the JS facades, with no Scala niceness, to React's test utilities.

  • japgolly.scalajs.react.test.ReactTestUtils is now a concrete object instead of a facade, and so all the additional utilities that were patched on with implicits earlier, are normal methods that live directly in the object - no more different behaviour depending on imports.

  • Added ReactTestUtils.{modify,replace}Props which can be used to test a component when its parent changes and re-renders it. Great for testing hooks like component{Will,Did}Update, componentWillReceiveProps, shouldComponentUpdate.

  • ReactTestVar has been revised.

    • It now provides StateSnapshot (with and without Reusability) and StateAccess. This is fantastic because instead of 3 separate utilities to test different types of code, there's now a single one that covers them all. Simple, easy to learn and use, less to remember, awesome.
    • It has an onUpdate hook. You'll usually use this to feed component updates back into itself. Example below.
  • Reduction: ComponentTester has been removed. Instead use ReactTestUtils.withRenderedIntoDocument followed by invocations of ReactTestUtils.{modify,replace}Props.

  • Reduction: WithExternalCompStateAccess has been removed. Instead create a ReactTestVar and use .stateAccess and .onUpdate. Example:

    val component: ScalaComponent[StateAccessPure[Int], Unit, Unit] = ...
    
    val testVar = ReactTestVar(1)
    ReactTestUtils.withRenderedIntoDocument(component(testVar.stateAccess)) { m =>
      testVar.onUpdate(m.forceUpdate) // Update the component when it changes the state
    
      assert(m.outerHtmlScrubbed() == "<div>1</div>")
      Simulate.click(m.getDOMNode) // our example component calls .modState(_ + 1) onClick
      assert(testVar.value() == 2)
      assert(m.outerHtmlScrubbed() == "<div>2</div>")
    }

Other

  • Cats module, ext-cats

    Where as before there was just optional support for Scalaz, there is now optional, analagous suppport for Cats. You're free to use one, the other, none or both.

    (The core, extra and test modules all get by with just plain 'ole Scala.)

  • ScalaDoc

    The ScalaDoc was pretty thin before, the reasoning being that all the documentation is on the project page under doc/. A number of people lamented its lack and so as of 1.0.0 there is now more ScalaDoc in the library. Over time, more will be added but I don't get paid for any work on this library unfortunately so please get involved if you'd like to see more.

Migration

As usual, there is a migration script that will update the majority of your code base. This time however, there are also a bunch of things you'll have to update manually.

Automatic migration

Go to your project directory. Make sure you've checked everything into version-control before you begin.
Then run:

curl -s https://raw.githubusercontent.com/japgolly/scalajs-react/master/doc/changelog/1.0.0-migrate.sh | bash

Alternatively you can open up 1.0.0-migrate.sh in your browser, inspect it, ensure it doesn't do anything nefaious or that you're uncomfortable with, then copy-paste it into your shell as a single command.

Remaining manual migration

Make sure you've done the automated migration first.

These are the breaking changes that must be manually updated:

  • Upgrade Scala.JS to 0.6.15
  • If you're using any of these libraries, upgrade them to:
  • VDOM changes
    • See the VDOM cheatsheet.
    • (a: TagMod) + (b: TagMod) + … becomes TagMod(a, b, …).
    • div(x: Seq[_ <% TagMog]) becomes div(x.toTagMod). You can also use div(x: _*) but the Scala language will impose more usage restrictions. If you find yourself with div(x.map(y).toTagMod) you can also change it to div(x.toTagMod(y)) for brevity and efficiency.
    • The ?= operator has been removed. Replace condition ?= vdom with vdom.when(condition) or vdom.unless(condition).
    • The := operator was overloaded before to handle optional cases (i.e. attr := Option[value]). Change to attr :=? Option[value] which is now consistent with the event operators --> and ==> having -->? and ==>? respectively.
    • Previously, the main package (japgolly.scalajs.react) had some vdom-related code in it. Now, all VDOM code is in the .vdom package. This means that in cases where you aren't importing from .vdom but are using certain VDOM features you'll need to add an import.
      // VdomElement not found
      import japgolly.scalajs.react.vdom.VdomElement
      
      // No implicit view available from Blah => VdomElement
      import japgolly.scalajs.react.vdom.Implicits._
  • String-based refs (deprecated by React JS) have been removed. See REFS.md for how to use the new style refs.
  • JS-based components now have first-class support. Old hacky code that had casts to confusing types like ReactComponentM_ will need to be replaced. See INTEROP.md.
  • The unpleasant $ field on Scala component lifecycle hooks has now been removed. Use auto-complete to get what you need directly from the hook context.
    // So instead of this:
    .componentDidUpdate(ctx => doSomething(ctx.$.props, ctx.$.backend))
    
    // it becomes something like this
    .componentDidUpdate(ctx => doSomething(ctx.currentProps, ctx.backend))
    
    // If you really want to bypass the ctx and directly access the underlying component,
    // use one of the .mounted fields.
    .componentDidUpdate(ctx => ajaxThing(ctx.mountedImpure))
  • Change custom mixins (that you use in .configure when creating Scala components):
    def install[P, S, B, N <: TopNode] =
      (_: ReactComponentB[P, S, B, N]).blah
    
    // Change ↑ to ↓
    
    def install[P, C <: Children, S, B]: ScalaComponent.Config[P, C, S, B] =
      _.blah
  • The implicit helpers .tryFocus (and its generalisation .tryTo), have been removed. Use autoFocus := true in your VDOM instead.
  • CompStateAccess[S] is now one of the following depending on what you're doing: (See TYPES.md)
    • StateAccessPure[S]
    • StateAccessImpure[S]
    • StateAccess[F, S]
    • StateAccessor.*
  • .getDOMNode is now consistent in that it obeys the effect type like everything else. Eg. if .props and .state return Callbacks, then .getDOMNode will too.
  • Mounted components' .isMounted now returns Option[Boolean] as not all components support it. It seems it's going to be deprecated and removed in React 16.

Migration notes

The migration script takes care of these but it's also worth highlighting these so you know:

  • Events (ReactEvent*) have been renamed.
    • SyntheticEvent[N]ReactEventFrom[N]
    • ReactEventIReactEventFromInput
    • ReactEventHReactEventFromHtml
    • ReactEventTAReactEventFromTextArea
  • VDOM - Seriously, check out VDOM.md.
    There are heaps of differences and a cheatsheet.
  • CallbackB alias removed. Just use CallbackTo[Boolean].
  • ReusableFn(x).{set,mod}State is now Reusable.fn.state(x).{set,mod}