This document explains the core features of Slinky and the technical decisions made when implementing them.
To make components type-safe in the data they handle, Slinky requires the component implementation class to be defined inside a Scala object that contains the dependent-object-types
State. Most of the time,
State are defined as case classes, the primary way to model immutable data in Scala, but can also be defined to alias external types.
With this API, a simple component looks like this:
ComponentWrapper class defines the abstract
State types as well as the
Definition type, which is a layer on top of React’s Component class. At runtime, the
Def class is converted into a React Component, which can be constructed to add to an HTML tree. This process is handled by the apply method in ComponentWrapper, which takes the properties to pass into the constructed component and returns a
ReactElement, which is the type used to represent constructed instances of components as well as other React tree types such as HTML trees, React portals, and React fragments.
Macro Annotation Components
@react macro annotation API.
One of Scala’s most powerful features is the ability to plug into compile phases and add custom behavior, such as modification of ASTs or generating custom files (which projects like Scala.js and Scala Native take advantage of to target platforms other than the JVM). Scala.meta is a library that gives developers a high-level API for code transformations, tapping into an early phase of the compiler to automatically run these transformations at compile time. Slinky uses this library to generate the classes for components, allowing developers to write their code with no boilerplate. To request Slinky to generate these classes, developers simply need to annotate their component code with
@react, which is detected at compile time to call Slinky’s AST transformer.
In addition to being run by the Scala compiler, IDEs like IntelliJ IDEA also integrate with custom macro annotations like
@react, so developers get code assistance based on the final generated code. Slinky strives to make developer experience as smooth as possible. To this effect, it ensures that its annotation processing is compatible with current IDEs, even when it requires significant engineering effort specific to individual developer tools.
Readers and Writers
Some React libraries, like
The Reader/Writer API builds upon typeclasses, a concept popularized by the Haskell programming language that makes it easy to define and implement behavior tied to specific types. Typeclasses can be defined for a variety of extensions like serialization, where individual typeclass instances handle the serialization of specific types. The powerful idea of typeclasses is that they can be composed, with complex typeclasses based on simpler ones for primitive types.
Slinky includes the
Promises and Scala
Coming back to the concept of generating code at compile time with macros, Slinky also includes macros (powered by Magnolia) that automatically generate typeclasses for case classes. So a case class like this:
can have typeclasses recursively generated like this (not legal Scala code; shortened to illustrate the core ideas)
With this API, a user simply needs to define the props type and provide a reference to the React component to render, and Slinky handles the rest!
In Slinky, enabling the hot-loading features is a one liner, but many changes go on behind the scenes to set up the application for preserving its state across multiple instances of Scala.js code