This will attempt to show you how to use React in Scala.
It is expected that you know how React itself works.
-
Add Scala.js to your project.
-
Add scalajs-react to SBT:
There are a number of different modules available. On this page we'll just use the
core
module but refer to the Modules doc to see other module options.
// "core" = essentials only. No bells or whistles.
libraryDependencies += "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1"
-
Add React to your build.
How to do this depends on your Scala.JS config and build setup.
If you're using scalajs-bundler, add the following SBT settings to get started:
enablePlugins(ScalaJSPlugin) enablePlugins(ScalaJSBundlerPlugin) libraryDependencies += "com.github.japgolly.scalajs-react" %%% "core" % "2.1.1" Compile / npmDependencies ++= Seq( "react" -> "17.0.2", "react-dom" -> "17.0.2")
If you're using old-school
jsDependencies
, add something akin to:// React JS itself (Note the filenames, adjust as needed, eg. to remove addons.) jsDependencies ++= Seq( "org.webjars.npm" % "react" % "17.0.2" / "umd/react.development.js" minified "umd/react.production.min.js" commonJSName "React", "org.webjars.npm" % "react-dom" % "17.0.2" / "umd/react-dom.development.js" minified "umd/react-dom.production.min.js" dependsOn "umd/react.development.js" commonJSName "ReactDOM", "org.webjars.npm" % "react-dom" % "17.0.2" / "umd/react-dom-server.browser.development.js" minified "umd/react-dom-server.browser.production.min.js" dependsOn "umd/react-dom.development.js" commonJSName "ReactDOMServer"),
See here for tips on configuring your IDE.
See VDOM.md.
See CALLBACK.md.
This is how to create components from Scala. (For JS components, see INTEROP.md.)
There is a component builder DSL beginning at ScalaComponent.build
.
You throw types and functions at it, call build
and when it compiles you will have a React component.
- The first step is to specify your component's properties type, and a component name.
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
object MyComponent {
case class Props(/* TODO */)
val Component =
ScalaComponent.builder[Props]
|
}
-
(Optional) If you want a stateful component, call one of the methods beginning with
.initialState
. Use your IDE to see the methods and the differences in their type signatures. -
(Optional) If you want a backend (explained below) for your component (and you do for non-trivial components), call
.backend
. If your backend has a.render
function, instead of.backend
here you can call.renderBackend
which will use a macro to instantiate your backend, and automatically choose the appropriate.render
function in the next step, bypassing it for you. -
Choose from one of the many available
render
functions. Use your IDE to see the methods and the differences in their type signatures. Alternatively, if you (for whatever reason) manually created a backend in the previous step and your backend has arender
function, you can call.renderBackend
here to have the builder automatically select the appropriaterender
function. -
(Optional) Type in the name of one of the React lifecycle hooks (eg.
componentDidMount
) to add that hook to your component. -
Call
.build
and you're done.
If your props is a singleton type (eg.Unit
) then the buider automatically (i.e. via implicit resolution) provides your component aCtorType
that doesn't the props be specified. (See TYPES.md for more info.)
Example with props:
val Hello =
ScalaComponent.builder[String]
.render_P(name => <.div("Hello there ", name))
.build
// Usage:
Hello("Draconus")
Example without props:
val NoArgs =
ScalaComponent.builder[Unit]
.renderStatic(<.div("Hello!"))
.build
// Usage:
NoArgs()
In addition to props and state, if you look at the React samples you'll see that most components need additional functions and even, (in the case of React's second example, the timer example), state outside of the designated state object (!). In plain React with JS, functions which can have access to the component's props and state (such as helpers fns and event handlers), are placed within the body of the component class. In scalajs-react you need another place for such functions as scalajs-react emphasises type-safety and provides different types for the component's scope at different points in the lifecycle. Instead they should be placed in some arbitrary class you may provide, called a backend.
See the online timer demo for an example.
As mentioned above, for the extremely common case of having a backend class with a render
method,
the builder comes with a .renderBackend
method.
It will locate the render
method, determine what the arguments need (props/state/propsChildren) by examining the
types or the arg names when the types are ambiguous, and create the appropriate function at compile-time.
If can also automate the creation of the backend, see below.
Example before: (yuk!)
type State = Vector[String]
class Backend(bs: BackendScope[Unit, State]) {
def render: VdomElement = {
val s = bs.state.runNow() // yuk!! .runNow() is unsafe
<.div(
<.div(s.length, " items found:"),
<.ol(s.toTagMod(i => <.li(i))))
}
}
val Example = ScalaComponent.builder[Unit]
.initialState(Vector("hello", "world"))
.backend(new Backend(_))
.render(_.backend.render)
.build
After:
class Backend(bs: BackendScope[Unit, State]) {
def render(s: State): VdomElement = // ← Accept props, state and/or propsChildren as argument
<.div(
<.div(s.length, " items found:"),
<.ol(s.toTagMod(i => <.li(i))))
}
val Example = ScalaComponent.builder[Unit]
.initialState(Vector("hello", "world"))
.renderBackend[Backend] // ← Use Backend class and backend.render
.build
You can also create a backend yourself and still use .renderBackend
:
val Example = ScalaComponent.builder[Unit]
.initialState(Vector("hello", "world"))
.backend(new Backend(_)) // ← Fine! Do it yourself!
.renderBackend // ← Use backend.render
.build
Once you've created a Scala React component, it mostly acts like a typical Scala case class. To use it, you create an instance. To create an instance, you call the constructor.
val NoArgs =
ScalaComponent.static(<.div("Hello!"))
val Hello =
ScalaComponent.builder[String]
.render_P(name => <.div("Hello there ", name))
.build
// Usage
<.div(
NoArgs(),
Hello("John"),
Hello("Jane"))
To add a key to a component instance, call .withKey(key)
before instantiation.
Examples:
<.div(
VdomArray(
NoArgs.withKey("noargs")(),
Hello.withKey("john")("John"),
Hello.withKey("jane")("Jane")))
See REFS.md.
With React JS you'd call ReactDOM.render(comp, target, callback?)
to render a component into DOM.
With scalajs-react, (unmounted) components come with a .renderIntoDOM(target, callback?)
method.
import org.scalajs.dom.document
NoArgs().renderIntoDOM(document.body)
-
Where
setState(State)
is applicable, you can also run:modState(State => State)
modState((State, Props) => State)
setStateOption(Option[State])
modStateOption(State => Option[State])
modStateOption((State, Props) => Option[State])
-
React has a classSet addon for specifying multiple optional class attributes. The same mechanism is applicable with this library is as follows:
<.div( ^.classSet( "message" -> true, "message-active" -> true, "message-important" -> props.isImportant, "message-read" -> props.isRead), props.message) // Or for convenience, put all constants in the first arg: <.div( ^.classSet1( "message message-active", "message-important" -> props.isImportant, "message-read" -> props.isRead), props.message)
-
Sometimes you want to allow a function to both get and affect a portion of a component's state. Anywhere that you can call
.setState()
you can also call.zoomState()
to return an object that has the same.setState()
,.modState()
methods but only operates on a subset of the total state.def incrementCounter(s: StateAccessPure[Int]): Callback = s.modState(_ + 1) // Then in some other component: case class State(name: String, counter: Int) def render = { val f = $.zoomState(_.counter)(value => _.copy(counter = value)) button(onclick --> incrementCounter(f), "+") }
You can cut down on boilerplate by using Monocle and the scalajs-react Monocle extensions. By doing so, the above snippet will look like this:
import monocle.macros._ @Lenses case class State(name: String, counter: Int) def render = { val f = $ zoomStateL State.counter button(onclick --> incrementCounter(f), "+") }
-
The
.getDOMNode
callback can sometimes execute when unmounted which is an increasingly annoying bug to track down. Since React 16 with its new burn-it-all-down error handling approach, an occurance of this can be fatal. In order to properly model the reality of the callback and ensure compile-time safety, rather than just getting back a VDOM reference, the return type is an ADT like this:ComponentDom ↑ ↑ ComponentDom.Mounted ComponentDom.Unmounted ↑ ↑ ComponentDom.Element ComponentDom.Text
Calling
.getDOMNode
from without lifecycle callbacks, returns aComponentDom.Mounted
. Calling.getDOMNode
on a mounted component instance or aBackendScope
now returnsComponentDom
which may or may not be mounted. Jump into theComponentDom
source to see the available methods but in most cases you'll use one of the following:trait ComponentDom { def mounted : Option[ComponentDom.Mounted] def toElement: Option[dom.Element] def toText : Option[dom.Text]
In unit tests you'll typically use
asMounted().asElement()
orasMounted().asText()
for inspection.
-
table(tr(...))
will appear to work fine at first then crash later. React needstable(tbody(tr(...)))
. -
React's
setState
functions are asynchronous; they don't apply invocations ofthis.setState
until the end ofrender
or the current callback. Calling.state
after.setState
will return the initial, original value, i.e.val s1 = $.state val s2 = "new state" $.setState(s2) $.state == s2 // returns false $.state == s1 // returns true
If this is a problem you have 2 choices.
- Use
modState
. - Refactor your logic so that you only call
setState
once.
- Use
-
Since
setState
andmodState
return callbacks, if you need to call them from outside of a component (e.g. by accessing the backend of a mounted component), call.runNow()
to trigger the change; else the callback will never run. See the Callbacks section for more detail.