Collaborative Realtime Clicker with Server-Side Rendering
This article is a short introduction to some of TrailBase more advanced features, specifically server-side rendering with a popular JS framework (React, Vue, Svelte, Solid) and realtime subscriptions to data changes.
We’ll built a simple clicker game, where players can increment a globally shared counter by click a button and updates are automatically send out to all participants.
The conclusion of this tutorial is part of the main code repository and can be found here or downloaded by running:
Creating an SSR Template Project
We start by setting up a server-side rendered (SSR) application that will
ultimately render in TrailBase’s JS/TS runtime.
Because it’s JavaScript there’s no single way to set up a new project, there’s
a million good ones and a few more questionable ones 😅.
For this tutorial we’ll use vite
, which has gained notable traction and
provides templates for most popular frameworks. As for frameworks, we’ll use
Solid but the same approach applies to React, Vue, Svelte, Preact, … .
Let’s run:
which will bring up a CLI assistant. Navigate through: Others > create-vite-extra > ssr-solid > TypeScript
. If you feel adventures, you can select any of the
other SSR options (React, Svelte, …) or go with JavaScript instead.
With the project template created, we can simply follow the on-screen instructions:
The final command, will start an express.js
development server run by Node.js as defined by <project-name>/server.js
.
Opening the
server.js
implementation,
marginally simplified it looks something like:
The details are not super important and framework-dependent but in simple terms:
- The server first loads and executes a render function defined in
dist/server/entry-server.js
. - Then cuts up an
HTML template
and string-replaces two parts:
- places a serialized “hydration” script into the header,
- and puts the output of the render function as the body of the HTML document.
With the actual rendering delegated to the respective JS framework, this is really all that’s needed for SSR. In other words, the above steps is all our TrailBase JS/TS handler will have to do.
Before we start plumbing it might be worth pointing out that everything under
dist/
are artifacts vite produces from their respective counterparts under
src/
.
For example, we can take a look at the above render function peeking into
src/entry-server.tsx
. Besides the dist/server
artifacts, there are
dist/client
artifacts. These are simply static content (HTML, CSS, JS)
intended for the user’s browser, which we can serve using
trail run --public-dir=<client-artifacts-path>
without further intervention.
Implementing the SSR Handler
As discussed above, we ultimately want to execute something akin to:
to put the body along some hydration script into our
<project-name>/index.html
template.
Let first address a minor obstacle. Running pnpm build
and looking at the
dist/server/entry-server.js
artifact we can see that the code is non-hermetic
depending on framework modules.
In order to make our lives easier, let’s inline everything by changing
vite.config.ts
to:
Running pnpm build
again, we have a standalone renderer without external
dependencies 👍.
We can now implement the handler in an existing TrailBase setup (or simply run
trail run
once before to generate a traildepot
directory) by creating a
traildepot/scripts/main.ts
Voila! Last things to do, satisfy the implicit dependencies:
- Satisfy the import by copying
dist/server/entry-server.js
totraildepot/scripts/
. - Satisfy the template loading by ensuring
dist/client/index.html
is a valid path relative to thetrail
process’ current working directory or update the path.
Lastly we need to pass the correct path to the client HTML, CSS and JS
artifacts as --public-path
:
With that, we have our SSR app running with TrailBase 🎉.
Building the Actual App
If SSR is what you’re after, the tutorial could end here. If you have a few more minutes, let’s build something a little more fun: a clicker app to collaborative increment a globally shared counter 🤣.
For this we need a few foundational pieces:
- Create a table with a single record holding the current global counter value.
- A way to forward the counter state from the initial server-side render to the client in a structured manner so we can continue to increment it1.
- An API to increment the counter.
- Add the actual button and a subscription to get notified whenever someone else increments the counter.
The following sections will cover the major beats but skip over some incrementally evolving glue code to avoid being overly redundant. Hopefully you can work things out with a little help from the compiler. If not, you can find the final product here to read along or after finishing the tutorial.
1. Setting up the Database
Using TrailBase’s migrations, we can simply add a migration file that will create the schema and the counter entry:
Note, if you’re working with a preexisting TrailBase setup, the timestamp in the file name needs to be larger than your most recent migration.
2. Forwarding Server State
We’ll simply serialize our state to JSON and inject it as a simple JS script into the HTML document next to the hydration script.
Afterwards we can update the client’s entry-point to read the state object and inject it into the JS app using the appropriate mechanism for your framework choice:
We also need to update the handler code to query the current count and wire it into the server-side render function above:
3. Counter Increment API
We could use record APIs and first read the current value
and subsequently
write value+1
, however this would lead to data races potentially loosing
concurrent updates and under-counting.
Instead we want an atomic database update. For this we create a new TS endpoint
in traildepot/scripts/main.ts
:
We’ll see in the following paragraph how this endpoint can be called from
src/App.tsx
.
4. UI & Record Subscription
We simply need to expose the counter
table through a world-readable record
API (using the admin UI or config file):
Afterwards we can build the UI and use the trailbase
client library to
subscribe as follows:
Conclusion
At this point you should hopefully have a little collaborative clicker game 🎉. If you got lost along the way - apologies . You can check out the final result here.
What’s Next?
Thanks for making it to the end. Beyond the basic example above, the repository contains a more involved examples, such as:
- A Blog with both, a Web and Flutter UI, more complex APIs, authorization and custom user profiles.
Any questions or suggestions? Reach out on GitHub and help us improve the docs. Thanks!
Footnotes
-
We could technically parse the value out of the rendered DOM, though we’d like to be a bit more robust against future changes to the DOM. ↩