aboutsummaryrefslogtreecommitdiff
path: root/docs/guide/01-quickstart.md
blob: f8723a78bc375baf85a0131755711866be39b466 (plain)
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# 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
something on the screen.

## What is a SPA?

Lustre can be used to create HTML in many different contexts, but it is primarily
designed to be used to build Single-Page Applications – or SPAs. SPAs are a type
of Web application that render content primarily in the browser (rather than on
the server) and, crucially, do not require a full page load when navigating
between pages or loading new content.

To help build these kinds of applications, Lustre comes with an opinionated
runtime. Some of Lustre's core features include:

- **Declarative rendering**: User interfaces are constructed using a declarative
  API that describes HTML as a function of your application's state. This is in
  contrast to more traditional imperative approaches to direct DOM mutation like
  jQuery.

- **State management**: If UIs are a function of state, then orchestrating state
  changes is crucial! Lustre provides a simple message-based state management
  system modelled after OTP [gen_servers](https://www.erlang.org/doc/design_principles/gen_server_concepts),
  Gleam's [actors](https://hexdocs.pm/gleam_otp/gleam/otp/actor.html), and the
  [Elm Architecture](https://guide.elm-lang.org/architecture/).

- **Managed side effects**: Managing asynchronous operations like HTTP requests
  and timers can be tricky when JavaScript is single-threaded. Lustre provides a
  runtime to manage these side effects and let them communicate with your application
  using the same messages as your update loop.

## Your first Lustre program

To get started, let's create a new Gleam application and add Lustre as a dependency.

```sh
$ gleam new app && cd app && gleam add lustre
```

By default, Gleam builds projects for the Erlang target unless told otherwise. We
can change this by adding a `target` field to the `gleam.toml` file generated in
the root of the project.

```diff
  name = "app"
+ target = "javascript"
  version = "1.0.0"

  ...
```

The simplest type of Lustre application is constructed with the `element` function.
This produces an application that renders a static piece of content without the
typical update loop.

We can start by importing `lustre` and `lustre/element` and just rendering some
text:

```gleam
import lustre
import lustre/element

pub fn main() {
  lustre.element(element.text("Hello, world!"))
}
```

Lustre includes tooling like a server to serve your application in development.
You can start that server by running:

```sh
$ gleam run -m lustre dev
```

The first time you run this command might take a little while, but subsequent runs
should be much faster!

> **Note**: Lustre uses esbuild under the hood, and attempts to download the [right
> binary for your platform](https://esbuild.github.io/getting-started/#download-a-build).
> If you're not connected to the internet, on an unsupported platform, or don't
> want Lustre to download the binary you can grab or build it yourself and place it
> in `build/.lustre/bin/esbuild`.

Once the server is up and running you should be able to visit http://localhost:1234
and be greeted with your "Hello, world!" message.

We mentioned Lustre has a declarative API for constructing HTML. Let's see what
that looks like by building something slightly more complex.

```gleam
import lustre
import lustre/attribute
import lustre/element
import lustre/element/html

pub fn main() {
  lustre.element(
    html.div([], [
      html.h1([], [element.text("Hello, world!")]),
      html.figure([], [
        html.img([attribute.src("https://cataas.com/cat")]),
        html.figcaption([], [element.text("A cat!")])
      ])
    ])
  )
}
```

Here we _describe_ the structure of the HTML we want to render, and leave the
busywork to Lustre's runtime: that's what makes it declarative!

"**Where are the templates?**" we hear you cry. Lustre doesn't have a separate
templating syntax like JSX or HEEx for a few reasons (lack of metaprogramming
built into Gleam, for one). Some folks might find this a bit odd at first, but
we encourage you to give it a try. Realising that your UI is _just functions_
can be a bit of a lightbulb moment as you build more complex applications.

## Adding interactivity

Rendering static HTML is great, but we said at the beginning Lustre was designed
primarily for building SPAs – and SPAs are interactive! To do that we'll need
to move on from `lustre.element` to the first of Lustre's application constructors
that includes an update loop: `lustre.simple`.

```gleam
import gleam/int
import lustre
import lustre/element
import lustre/element/html
import lustre/event

pub fn main() {
  lustre.simple(init, update, view)
}
```

There are three main building blocks to every interactive Lustre application:

- A `Model` that represents your application's state and an `init` function
  to create it.

- A `Msg` type that represents all the different ways the outside world can
  communicate with your application and an `update` function that modifies
  your model in response to those messages.

- A `view` function that renders your model to HTML.

We'll build a simple counter application to demonstrate these concepts. Our
model can be an `Int` and our `init` function will initialise it to `0`:

```gleam
pub type Model = Int

fn init(_) -> Model {
  0
}
```

> **Note**: The `init` function always takes a single argument! These are the "flags"
> or start arguments you can pass in when your application is started with
> `lustre.start`. For the time being, we can ignore them, but they're useful for
> passing in configuration or other data when your application starts.

The main update loop in a Lustre application revolves around messages passed in
from the outside world. For our counter application, we'll have two messages to
increment and decrement the counter:

```gleam
pub type Msg {
  Increment
  Decrement
}

pub fn update(model: Model, msg: Msg) -> Model {
  case msg {
    Increment -> model + 1
    Decrement -> model - 1
  }
}
```

Each time a message is produced from an event listener, Lustre will call your
`update` function with the current model and the incoming message. The result
will be the new application state that is then passed to the `view` function:

```gleam
pub fn view(model: Model) -> lustre.Element(Msg) {
  let count = int.to_string(model)

  html.div([], [
    html.button([event.click(Increment)], [
      element.text("+")
    ]),
    element.text(count),
    html.button([event.click(Decrement)], [
      element.text("-")
    ])
  ])
}
```

The above snippet attaches two click event listeners that produce an `Increment`
or `Decrement` message when clicked. The Lustre runtime is responsible for
attaching these event listeners and calling your `update` function with the
resulting message.

**Note**: notice that the return type of `view` is `lustre.Element(Msg)`. The
type parameter `Msg` tells us the kinds of messages this element might produce
from events: type safety to the rescue!

This forms the core of every Lustre application:

- A model produces some view.
- The view can produce messages in response to user interaction.
- Those messages are passed to the update function to produce a new model.
- ... and the cycle continues.

## Talking to the outside world

This "closed loop" of messages and updates works well if all we need is an
interactive document, but many applications will also need to talk to the outside
world – whether that's fetching data from an API, setting up a WebSocket connection,
or even just setting a timer.

Lustre manages these side effects through an abstraction called an `Effect`. In
essence, effects are any functions that talk with the outside world and might
want to send messages back to your application. Lustre lets you write your own
effects, but for now we'll use a community package called
[`lustre_http`](https://hexdocs.pm/lustre_http/index.html) to fetch a new cat image
every time the counter is incremented.

Because this is a separate package, make sure to add it to your project first.
While we're here, we'll also add `gleam_json` so we can decode the response from
the cat API:

```sh
$ gleam add gleam_json lustre_http
```

Now we are introducing side effects, we need to graduate from `lustre.simple` to
the more powerful `lustre.application` constructor.

```gleam
import gleam/int
import lustre
import lustre_http
import lustre/attribute
import lustre/element
import lustre/element/html
import lustre/event

pub fn main() {
  lustre.application(init, update, view)
}
```

If you edited your previous counter app, you'll notice the prorgam no longer
compiles. Specifically, the type of our `init` and `update` functions are wrong
for the new `lustre.application` constructor!

In order to tell Lustre about what effects it should perform, these functions now
need to return a _tuple_ of the new model and any effects. We can amend our `init`
function like so:

```gleam
pub type Model {
  Model(count: Int, cats: List(String))
}

fn init(_) -> #(Model, effect.Effect(Msg)) {
  #(Model(0, []), effect.none())
}
```

The `effect.none` function is a way of saying "no effects" – we don't need to do
anything when the application starts. We've also changed our `Model` type from a
simple type alias to a Gleam [record](https://tour.gleam.run/data-types/records/)
that holds both the current count and a list of cat image URLs.

In our `update` function, we want to fetch a new cat image every time the counter
is incremented. To do this we need two things:

- An `Effect` to describe the request the runtime should perform.
- A variant of our `Msg` to handle the response.

The `lustre_http` package has the effect side of things handled, so we just need
to modify our `Msg` type to include a new variant for the response:

```gleam
pub type Msg {
  Increment
  Decrement
  GotCat(Result(String, lustre_http.HttpError))
}
```

Finally, we can modify our `update` function to also fetch a cat image when the
counter is incremeneted and handle the response:

```gleam
pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) {
  case msg {
    Increment -> #(Model(..model, count: model.count + 1), get_cat())
    Decrement -> #(Model(..model, count: model.count - 1), effect.none())
    GotCat(Ok(cat)) -> #(Model(..model, cats: [cat, ..model.cats]), effect.none())
    GotCat(Error(_)) -> #(model, effect.none())
  }
}

fn get_cat() -> effect.Effect(Msg) {
  let decoder = dynamic.field("_id", dynamic.string)
  let expect = lustre_http.expect_json(decoder, GotCat)

  lustre_http.get("https://cataas.com/cat?json=true", expect)
}
```

**Note**: The `get_cat` function returns an `Effect` that tells the runtime how
to fetch a cat image. It's important to know that the `get_cat` function doesn't
perform the request directly! This is why we need to add the `GotCat` message
variant: the runtime needs to know what to do with the response when it arrives.

This model of managed effects can feel cumbersome at first, but it comes with some
benefits. Forcing side effects to produce a message means our message type naturally
describes all the ways the world can communicate with our application; as an app
grows being able to get this kind of overview is invaluable! It also means we can
test our update loop in isolation from the runtime and side effects: we could write
tests that verify a particular sequence of messages produces an expected model
without needing to mock out HTTP requests or timers.

Before we forget, let's also update our `view` function to actually display the
cat images we're fetching:

```gleam
pub fn view(model: Model) -> lustre.Element(Msg) {
  let count = int.to_string(model.count)

  html.div([], [
    html.button([event.click(Increment)], [
      element.text("+")
    ]),
    element.text(count),
    html.button([event.click(Decrement)], [
      element.text("-")
    ]),
    html.div([], list.map(model.cats, fn(cat) {
      html.img([attribute.src(cat)])
    }))
  ])
}
```

## Where to go from here

Believe it or not, you've already seen about 80% of what Lustre has to offer! From
these core concepts, you can build rich interactive applications that are predictable
and maintainable. Where to go from here depends on what you want to build, and
how you like to learn:

- There are a number of [examples](https://github.com/lustre-labs/lustre/tree/main/examples)
  if the Lustre repository that gradually introduce more complex applications
  and ideas.

- The [rest of this guide](/guide/02-state-management) also continues to teach
  Lustre's high-level concepts and best-practices.

- If you're coming from LiveView or have heard about Lustre's server components
  and want to learn more, you can skip to the [server components](/guide/05-server-components)
  section of the guide to learn about how to run Lustre applications on the backend.

- Of course, if you want to dive in and start making things straight away, the
  [API documentation](/lustre) is always handy to keep open.

## 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).

While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org)
is also a great resource for learning about the Model-View-Update architecture
and the kinds of patterns that Lustre is built around.