diff options
Diffstat (limited to 'pages')
-rw-r--r-- | pages/guide/01-quickstart.md | 6 | ||||
-rw-r--r-- | pages/guide/03-side-effects.md | 2 | ||||
-rw-r--r-- | pages/guide/04-server-side-rendering.md | 219 |
3 files changed, 223 insertions, 4 deletions
diff --git a/pages/guide/01-quickstart.md b/pages/guide/01-quickstart.md index 87e8ace..9a0546f 100644 --- a/pages/guide/01-quickstart.md +++ b/pages/guide/01-quickstart.md @@ -1,4 +1,4 @@ -# 01 Quickstart Guide +# 01 Quickstart guide Welcome to the Lustre quickstart guide! This document should get you up to speed with the core ideas that underpin every Lustre application as well as how to get @@ -163,7 +163,7 @@ model can be an `Int` and our `init` function will initialise it to `0`: ```gleam pub type Model = Int -fn init(_) -> Model { +fn init(_flags) -> Model { 0 } ``` @@ -282,7 +282,7 @@ pub type Model { Model(count: Int, cats: List(String)) } -fn init(_) -> #(Model, effect.Effect(Msg)) { +fn init(_flags) -> #(Model, effect.Effect(Msg)) { #(Model(0, []), effect.none()) } ``` diff --git a/pages/guide/03-side-effects.md b/pages/guide/03-side-effects.md index 5306408..83bf4e7 100644 --- a/pages/guide/03-side-effects.md +++ b/pages/guide/03-side-effects.md @@ -120,7 +120,7 @@ We can, for example, launch an HTTP request on application start by using `lustr in our `init` function: ```gleam -fn init(_) { +fn init(_flags) { let model = Model(...) let get_ip = lustre_http.get( "https://api.ipify.org", 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). |