aboutsummaryrefslogtreecommitdiff
path: root/pages/guide/04-server-side-rendering.md
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2024-04-05 13:22:40 +0100
committerGitHub <noreply@github.com>2024-04-05 13:22:40 +0100
commitfa1f447bd0eaf9d27b6af407f90477b81e70b358 (patch)
tree9c72f3408e94fbe8b50a2390a0bd13d0754c19bc /pages/guide/04-server-side-rendering.md
parent99ba44fe0cc19cfc4191bd95ec6fccaea080a929 (diff)
downloadlustre-fa1f447bd0eaf9d27b6af407f90477b81e70b358.tar.gz
lustre-fa1f447bd0eaf9d27b6af407f90477b81e70b358.zip
🔀 Write a guide on server-side rendering. (#103)
* :memo: Fix typos in state management guide. * :memo: Begin writing ssr guide. * :memo: Rename discard init arg to '_flags' in all examples. * :memo: Show simple static HTML rendering example. * :memo: Expand hydration section. * :memo: Finish ssr guide. * :wrench: Add ssr guide to built docs pages.
Diffstat (limited to 'pages/guide/04-server-side-rendering.md')
-rw-r--r--pages/guide/04-server-side-rendering.md219
1 files changed, 219 insertions, 0 deletions
diff --git a/pages/guide/04-server-side-rendering.md b/pages/guide/04-server-side-rendering.md
new file mode 100644
index 0000000..b72d887
--- /dev/null
+++ b/pages/guide/04-server-side-rendering.md
@@ -0,0 +1,219 @@
+# 04 Server-side rendering
+
+Up until now, we have focused on Lustre's ability as a framework for building
+Single Page Applications (SPAs). While Lustre's development and feature set is
+primarily focused on SPA development, that doesn't mean it can't be used on the
+backend as well! In this guide we'll set up a small [mist](https://hexdocs.pm/mist/)
+server that renders some static HTML using Lustre.
+
+## Setting up the project
+
+We'll start by adding the dependencies we need and scaffolding the HTTP server.
+Besides Lustre and Mist, we also need `gleam_erlang` (to keep our application
+alive) and `gleam_http` (for types and functions to work with HTTP requests and
+responses):
+
+```sh
+gleam new app && cd app && gleam add gleam_erlang gleam_http lustre mist
+```
+
+Besides imports for `mist` and `gleam_http` modules, we also need to import some
+modules to render HTML with Lustre. Importantly, we _don't_ need anything from the
+main `lustre` module: we're not building an application with a runtime!
+
+```gleam
+import gleam/bytes_builder
+import gleam/http/request.{type Request}
+import gleam/http/response.{type Response}
+import lustre/element.{type Element}
+import lustre/element/html.{html}
+import mist.{type Connection, type ResponseData}
+```
+
+We'll modify Mist's example and write a simple request handler that responds to
+requests to `/greet/:name` with a greeting message:
+
+```gleam
+pub fn main() {
+ let empty_body = mist.Bytes(bytes_builder.new())
+ let not_found = response.set_body(response.new(404), empty_body)
+
+ let assert Ok(_) =
+ fn(req: Request(Connection)) -> Response(ResponseData) {
+ case request.path_segments(req) {
+ ["greet", name] -> greet(name)
+ _ -> not_found
+ }
+ }
+ |> mist.new
+ |> mist.port(3000)
+ |> mist.start_http
+
+ process.sleep_forever()
+}
+```
+
+Let's take a peek inside that `greet` function:
+
+```gleam
+fn greet(name: String) -> Response(ResponseData) {
+ let res = response.new(200)
+ let html =
+ html([], [
+ html.head([], [html.title([], "Greetings!")]),
+ html.body([], [
+ html.h1([], [html.text("Hey there, " <> name <> "!")])
+ ])
+ ])
+
+ response.set_body(res,
+ html
+ |> element.to_document_string
+ |> bytes_builder.from_string
+ |> mist.Bytes
+ )
+}
+```
+
+The `lustre/element` module has functions for rendering Lustre elements to a
+string (or string builder); the `to_document_string` function helpfully prepends
+the `<!DOCTYPE html>` declaration to the output.
+
+It's important to realise that `element.to_string` and `element.to_document_string`
+can render _any_ Lustre element! This means you could take the `view` function
+from your client-side SPA and render it server-side, too.
+
+## Hydration
+
+If we know we can render our apps server-side, the next logical question is how
+do we handle _hydration_? Hydration is the process of taking the static HTML
+generated by the server and turning it into a fully interactive client application,
+ideally doing as little work as possible.
+
+Most frameworks today support hydration or some equivalent, for example by
+serialising the state of each component into the HTML and then picking up where
+the server left off. Lustre doesn't have a built-in hydration mechanism, but
+because of the way it works, it's easy to implement one yourself!
+
+We've said many times now that in Lustre, your `view` is just a
+[pure function](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md)
+of your model. We should produce the same HTMl every time we call `view` with the
+same model, no matter how many times we call it.
+
+Let's use that to our advantage! We know our app's `init` function is responsible
+for producing the initial model, so all we need is a way to make sure the initial
+model on the client is the same as whate the server used to render the page.
+
+```gleam
+pub fn view(model: Int) -> Element(Msg) {
+ let count = int.to_string(model)
+
+ html.div([], [
+ html.button([event.on_click(Decr)], [html.text("-")]),
+ html.button([event.on_click(Incr)], [html.text("+")]),
+ html.p([], [html.text("Count: " <> count)])
+ ])
+}
+```
+
+We've seen the counter example a thousand times over now, but it's a good example
+to show off how simple hydration can be. The `view` function produces some HTML
+with events attached, but we already know Lustre can render _any_ element to a
+string so that shouldn't be a problem.
+
+Let's imagine our HTTP server responds with the following HTML:
+
+```gleam
+import app/counter
+import gleam/bytes_builder
+import gleam/http/response.{type Response}
+import gleam/json
+import lustre/attribute
+import lustre/element.{type Element}
+import lustre/element/html.{html}
+import mist.{type ResponseData}
+
+fn app() -> Response(ResponseData) {
+ let res = response.new(200)
+
+ let model = 5
+ let html =
+ html([], [
+ html.head([], [
+ html.script([attribute.type_("module"), attribute.src("...")], ""),
+ html.script([attribute.type_("application/json"), attribute.id("model")],
+ json.int(model)
+ |> json.to_string
+ )
+ ]),
+ html.body([], [
+ html.div([attribute.id("app")], [
+ counter.view(model)
+ ])
+ ])
+ ])
+
+ response.set_body(res,
+ html
+ |> element.to_document_string
+ |> bytes_builder.from_string
+ |> mist.Bytes
+ )
+}
+```
+
+We've rendered the shell of our application, as well as the counter using `5` as
+the initial model. Importantly, we've included a `<script>` tag with the initial
+model encoded as JSON (it might just be an `Int` in this example, but it could
+be anything).
+
+On the client, it's a matter of reading that JSON and decoding it as our initial
+model. The [plinth](https://hexdocs.pm/plinth/plinth.html) package provides
+bindings to many browser APIs, we can use that to read the JSON out of the script
+tag:
+
+```gleam
+import gleam/dynamic
+import gleam/json
+import gleam/result
+import lustre
+import plinth/browser/document
+import plinth/browser/element
+
+pub fn main() {
+ let json =
+ document.query_selector("#model")
+ |> result.map(element.inner_text)
+
+ let flags =
+ case json.decode_string(json, dynamic.int) {
+ Ok(count) -> count
+ Error(_) -> 0
+ }
+
+ let app = lustre.application(init, update, view)
+ let assert Ok(_) = lustre.start(app, "#app", flags)
+}
+```
+
+Hey that wasn't so bad! We made sure to fall back to an initial count of `0` if
+we failed to decode the JSON: this lets us handle cases where the server might
+not want us to hydrate.
+
+If you were to set this all up, run it, and check your browser's developer tools,
+you'd see that the existing HTML was not replaced and the app is fully interactive!
+
+For many cases serialising the entire model will work just fine. But remember
+that Lustre's super power is that pure `view` function. If you're smart, you can
+reduce the amount of data you serialise and _derive_ the rest of your model from
+that.
+
+We brushed over quite a few details showing how hydration could work here, but in
+the [next guide](https://hexdocs.pm/lustre/guide/05-full-stack-applications.html)
+we'll go into a lot more detail on how to set up and run a full-stack Lustre app.
+
+## Getting help
+
+If you're having trouble with Lustre or not sure what the right way to do
+something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
+You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).