Skip to content

Instantly share code, notes, and snippets.

@japgolly
Last active September 20, 2022 11:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save japgolly/7026294793e3e49030b126cd4ff521af to your computer and use it in GitHub Desktop.
Save japgolly/7026294793e3e49030b126cd4ff521af to your computer and use it in GitHub Desktop.
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))
router().renderIntoDOM(`#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 = {
p.page match {
case Page.Home => HomeComponent( //...
case Page.Login => LoginComponent( //...
case Page.Logout => LogoutComponent( //...
case Page.Whatever => WhateverComponent( //...
// etc
}
}
}
val Component = ScalaComponent.builder[Props]("LoadedRoot")
.initialState(State.init(cd))
.renderBackend[Backend]
.configure(Listenable.listen(_ => cd, _.backend.onProjectChange))
.build
}
@Mahoney
Copy link

Mahoney commented Sep 29, 2018

Anyone wanting to follow along with a compiling version - see my attempt at
https://gist.github.com/Mahoney/3ed811f6d5e5f5f32292cfc3d8004029

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