aboutsummaryrefslogtreecommitdiff
path: root/examples/05-http-requests/README.md
blob: 5fb95eb1e9641e29c3abbc2b2a03056c58533167 (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
![](./header.png)

# 05 HTTP Requests

In the previous examples, we've seen Lustre applications constructed with the
[`lustre.simple`](https://hexdocs.pm/lustre/lustre.html#simple) constructor.
These kinds of applications are great for introducing the Model-View-Update (MVU)
pattern, but for most real-world applications we'll need a way to talk to the
outside world.

Lustre's runtime includes _managed effects_, which allow us to perform side effects
like HTTP requests and communicate the results back to our application's `update`
function. To learn more about Lustre's effect system and why it's useful, check
out the [side effects guide](https://hexdocs.pm/lustre/guide/03-side-effects.html),
or the docs for the [`lustre/effect` module](https://hexdocs.pm/lustre/lustre/effect.html).

This example is a practical look at what effects mean in Lustre, and we'll look
at how to send HTTP requests in a Lustre application: a pretty important thing to
know!

## Moving on from `lustre.simple`

From this example onwards, we will use a new application constructor:
[`lustre.application`](https://hexdocs.pm/lustre/lustre.html#application). Full Lustre
applications have the ability to communicate to the runtime. Let's compare the type
of both the `simple` and `application` functions:

```gleam
pub fn simple(
  init: fn(flags) -> model,
  update: fn(model, msg) -> model,
  view: fn(model) -> Element(msg),
) -> App(flags, model, msg)

pub fn application(
  init: fn(flags) -> #(model, Effect(msg)),
  update: fn(model, msg) -> #(model, Effect(msg)),
  view: fn(model) -> Element(msg),
) -> App(flags, model, msg)
```

All that's changed is the return type of our `init` and `update` functions. Instead
of returning just a new model, they now return a tuple containing both a model and
any side effects we want the runtime to perform.

You'll notice that running a Lustre app with side effects _changes the signature_
of our [`init`](src/app.gleam#L43) and [`update`](src/app.gleam#L54) functions.
Instead of returning just a model, we return a tuple containing both a model an
an `Effect(Msg)` value. The effect value specifies any further updates we might
want the Lustre runtime to execute before the next invocation of the `view`
function.

> **Note**: notice how the type of `view` remains the same. In Lustre, your `view`
> is always a [_pure function_](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md)
> that takes a model and returns the UI to be rendered: we never perform side effects
> in the `view` function itself.

## HTTP requests as side effects

The community library [`lustre_http`](https://hexdocs.pm/lustre_http/) gives us
a way to model HTTP requests as Lustre `Effect`s. Crucially, when we call
`lustre_http.get` we are _not_ performing the request! We're constructing a
description of the side effect that we can hand off to the Lustre runtime to
perform.

```gleam
fn get_quote() -> Effect(Msg) {
  let url = "https://api.quotable.io/random"
  let decoder =
    dynamic.decode2(
      Quote,
      dynamic.field("author", dynamic.string),
      dynamic.field("content", dynamic.string),
    )

  lustre_http.get(url, lustre_http.expect_json(decoder, ApiUpdatedQuote))
}
```

To construct HTTP requests, we need a few different things:

- The `url` to send the request to.

- A description of what we _expect_ the result to be. There are a few options:
  `expect_anything`, `expect_text`, `expect_json`. In this example we say we're
  expecting a JSON response and provide a decoder.

- Along with what we expect the response to be, we also need to provide a way
  to turn that response into a `Msg` value that our `update` function can handle.

The same applies for post requests too, but there you also need to provide the
JSON body of the request.

## Tying it together

We now have a function that can create an `Effect` for us, but we need to hand it
to the runtime to be executed. The only way we can do that is by returning it from
our `update` (or `init`) function! We attach an event listener on a button, and
when the user clicks that button we'll return the `Effect` we want to perform as
the second element of a tuple:

```gleam
fn view(model: Model) -> Element(Msg) {
  ui.centre([],
    ui.button([event.on_click(UserClickedRefresh)], [
      element.text("New quote"),
    ]),
  )
}

fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    UserClickedRefresh -> #(model, get_quote())
    ...
  }
}
```

Of course, we need to handle responses from the quote API in our `update` function
too. When there are no side effects we want the runtime to perform for us, we need
to call `effect.none()`:

```gleam
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
  case msg {
    ...
    ApiUpdatedQuote(Ok(quote)) -> #(Model(quote: Some(quote)), effect.none())
    ApiUpdatedQuote(Error(_)) -> #(model, effect.none())
  }
}
```

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