Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Stateful SPA
// Overview of how this works:
// 1. Initialisation data is used to create an instance of LoadedRoot.
// 2. LoadedRoot contains a component which is the virtual top-level component.
// It uses the initialisation data and can be sure that it won't change (a guarantee you don't have with component props).
// It's the only component to have state.
// The state can applies to the entire SPA, all routes.
// It gets told by the router which page to render.
// It can house logic that applies when certain pages change to certain other pages.
// 3. LoadedRoot is passed to Routes.routerConfig.
// Routes.routerConfig creates a config that sends all routes to the LoadedRoot component, using the Page & RouterCtl as props.
// 4. Main creates a router using the RouterConfig and renders it.
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Main.scala
val root = new LoadedRoot(i, cp, cd)
val baseUrl = determineBaseUrl(dom.window.location.href)
val router = Router(baseUrl, Routes.routerConfig(root))
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
object Routes {
import japgolly.scalajs.react.extra.router.{RouterCtl => RouterCtl_, _}
type RouterCtl = RouterCtl_[Page]
sealed trait Page
// ... your SPA pages here
def routerConfig(rootInstance: LoadedRoot) =
RouterConfigDsl[Page].buildConfig { dsl =>
import dsl._
def render(page: Page, r: RouterCtl) =
rootInstance.Component(LoadedRoot.Props(page, r))
val dynPage = dynRenderR((page: Page, r) => render(page, r))
def staticPage(route: StaticDsl.Route[Unit], page: Page) =
staticRoute(route, page) ~> renderR(r => render(page, r))
// ...
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
import Routes.{Page, RouterCtl}
object LoadedRoot {
case class Props(page: Page, routerCtl: RouterCtl)
final class LoadedRoot(initData: ProjectSpaProtocols.InitData, cp: ClientProtocol, val cd: ClientData) {
final class Backend($: BackendScope[Props, State]) extends OnUnmount {
def render(p: Props, s: State): VdomElement = { match {
case Page.Home => HomeComponent( //...
case Page.Login => LoginComponent( //...
case Page.Logout => LogoutComponent( //...
case Page.Whatever => WhateverComponent( //...
// etc
val Component = ScalaComponent.builder[Props]("LoadedRoot")
.configure(Listenable.listen(_ => cd, _.backend.onProjectChange))

This comment has been minimized.

Copy link

commented Sep 29, 2018

Anyone wanting to follow along with a compiling version - see my attempt at

I still don't entirely understand the component lifecyle; I expected that applying new Props to the built Component would give me a new instance of the component and so reset its state to the initial state, but evidently not as I have tested it and the updated state is retained.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.