Usage with React
Although Diode is totally framework agnostic, it does work very well together with React. To simplify integration of Diode
with your scalajs-react application, use a separate library diode-react
.
To use Diode React in your application add following dependency declaration to your Scala.js project.
"io.suzaku" %%% "diode-react" % "1.1.5.142"
Overview
In React the user interface is built out of a hierarchy of components, each receiving props and optionally having internal state. With Diode you'll want most of your React components to be stateless because state is managed by the Diode circuit. Sometimes, however, it is useful to maintain some state in the UI component, for example when editing an item.
A very simple integration can be achieved just by passing a value from a model reader to your React component in its props. This works well for dummy leaf components that just display data. This approach, however, has a few downsides:
- Your component is not notified of any model changes
- Your component cannot dispatch any actions
Wrap and Connect Components
To simplify connecting your Diode circuit to your React components, Diode React provides a way to wrap and connect components. First step is to add the
ReactConnector
trait to your application Circuit
class.
case class RootModel(data: Seq[String], asyncData: Pot[String])
object AppCircuit extends Circuit[RootModel] with ReactConnector[RootModel] { ... }
Now your AppCircuit
is equipped with two new methods: wrap
and connect
. Both methods have almost the same signature, taking a zoom function (or
alternatively a model reader). Wrap also takes a function to build a React component immediately whereas connect returns a component taking a similar function as props.
def wrap[S, C](zoomFunc: M => S)(compB: ModelProxy[S] => C): C
def connect[S](zoomFunc: M => S): ReactConnectProxy[S]
The difference between these two is that wrap
just creates a ModelProxy
and passes it to your component builder function, while connect
actually creates
another React component that proxies your own component.
Use
wrap
when your component doesn't need model updates from Diode, andconnect
when it does. Even if you usewrap
on a top component, you can stillconnect
components underneath it.
When wrapping or connecting a component, you can either pass the ModelProxy
directly as props, or use it in the builder function to pass relevant props to
your component.
// connect with ModelProxy
val smartComponent = ScalaComponent.builder[ModelProxy[Seq[String]]]("SmartComponent").build
...
val sc = AppCircuit.connect(_.data)
// wrap with specific props
case class Props(data: Seq[String], onClick: Callback)
val dummyComponent = ScalaComponent.builder[Props]("DummyComponent").build
...
val dc = AppCircuit.wrap(_.data)(p => dummyComponent(Props(p(), p.dispatch(DummyClicked)))
def render = <.div(sc(p => smartComponent(p)), dc)
The ModelProxy
provides a dispatchCB
method that wraps the dispatch call in a React Callback
, making it easy to integrate with event
handlers etc. If you want to dispatch immediately, you can use dispatchNow
instead. It also provides wrap
and connect
methods, allowing your component to
connect sub-components to the Diode circuit.
Note that connect
is being called once for the lifecycle of this component. Having a single reference to this component during your components lifecycle
ensures that React will update your component rather than unmounting and remounting it. This applies to calling connect
in other contexts too. Try to connect
and store your component once and reuse it.
import scala.language.existentials // needed for the ReactConnectProxy in State
case class State(component: ReactConnectProxy[Pot[String]])
val Dashboard = ScalaComponent.builder[ModelProxy[RootModel]]("Dashboard")
.initialStateFromProps(proxy => State(proxy.connect(_.asyncData)))
.renderPS { (_, proxy, state) =>
<.div(
<.h3("Data"),
state.component(p => AsyncDataView(p)), // pass ModelProxy
proxy.wrap(_.data)(p => DataView(p())), // just pass the value
<.button(^.onClick --> proxy.dispatchCB(RefreshData), "Refresh")
)
}
.build
Rendering Pot
Because a Pot
can exist in many states, it's desirable to reflect these states in your view components. Diode React extends Pot
by adding convenient
rendering methods through ReactPot
.
// use import to get access to implicit extension methods
import diode.react.ReactPot._
An example from the SPA tutorial
val Motd = ScalaComponent.builder[ModelProxy[Pot[String]]]("Motd")
.render_P { proxy =>
Panel(Panel.Props("Message of the day"),
// render messages depending on the state of the Pot
proxy().renderPending(_ > 500, _ => <.p("Loading...")),
proxy().renderFailed(ex => <.p("Failed to load")),
proxy().render(m => <.p(m)),
Button(Button.Props(proxy.dispatchCB(UpdateMotd()), CommonStyle.danger), Icon.refresh, " Update")
)
}
Each of the rendering functions optionally renders the provided content, depending on the state of Pot
. In renderPending
you can supply a condition based
on the duration of the pending request, so that the UI will show a "Loading" message only after some time has elapsed. This of course requires that your action
is updating the model at suitable intervals for the model to update.
Examples
You may also want to take a look at the TodoMVC example for how to build React applications using Diode for application state management.
Another more complete application is example is the Scala.js SPA tutorial, demonstrating the use of
ReactPot
as well.