1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
|
# 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/erlang/process
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import lustre/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 what 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.
## 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).
|