Last active
September 20, 2022 11:20
-
-
Save japgolly/7026294793e3e49030b126cd4ff521af to your computer and use it in GitHub Desktop.
Stateful SPA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.