diff options
Diffstat (limited to 'pages/guide/02-state-management.md')
-rw-r--r-- | pages/guide/02-state-management.md | 84 |
1 files changed, 82 insertions, 2 deletions
diff --git a/pages/guide/02-state-management.md b/pages/guide/02-state-management.md index a9b52c5..b7aa31a 100644 --- a/pages/guide/02-state-management.md +++ b/pages/guide/02-state-management.md @@ -1,10 +1,71 @@ # 02 State management We saw in the qucikstart guide that all Lustre applications are built around the -Model View Update (MVU) architecture. This means that the state of the application +Model-View-Update (MVU) architecture. This means that the state of the application is stored in a single, immutable data structure called the model, and updated as messages are dispatched to the runtime. +The MVU architecture is an example of _unidirectional data flow_: + +- Your model describes the entire state of your application at a given point in + time. + +- The UI is a pure function of that model: if the model doesn't change, the UI + doesn't change. + +- Events from the outside world – user interaction, HTTP responses, ... – send + messages to an update function that constructs a new model. + +- The UI re-renders based on the new state. + +This is in contrast to _bidirectional_ approaches to state management, where the +UI can modify state directly. For some developers this can be a difficult idea +to get used to, but it brings a number of benefits: + +- A **single source of truth** makes it easier to reason about the state of your + application. State management is lifted _out_ of UI code, letting it focus just + on presentation and making it easier to test and refactor. + +- Message-driven **declarative state updates** give you a hollistic view of how + your application can change over time. Tracking incoming messages gives you a + history of state updates and can be serialised and logged for debugging or + testing purposes. + +- State updates are **pure**. We will learn more about this in the [next guide](./03-side-effects), + but for now it is enough to know that this means testing your state changes is + much easier because mocking messages is simpler tham mocking side effects! + +The rest of this guide contains some learned-wisdom and best practices for managing +state in Lustre applications. + +## The best model is not always a record + +It is overwhelmingly common to see the model of a Lustre application as a single +record. This is a sensible place to start, but there are other options! Gleam's +custom types allow us to model our data as disjoint variants. Using these as your +application's model can be particularly useful when you have different states that +do not need to persist across navigations: + +```gleam +type Model { + LoggedIn(LoggedInModel) + Public(PublicModel) +} + +type LoggedInModel { + ... +} + +type PublicModel { + ... +} +``` + +Here, we a model that represents our application as either having a logged in user +or just one of the public routes. This pushes us towards the great practice of +[making impossible states impossible](https://github.com/stereobooster/pragmatic-types/blob/master/posts/making-impossible-states-impossible.md). +Now, we can write separate + ## Messages not actions Lustre is not the first frontend framework to use the MVU architecture or to @@ -14,7 +75,26 @@ though, and these libraries often talk in terms of _actions_ but you'll see Elm and Lustre prefer the term _message_. Actions frame incoming events as _things to do_: "add a new todo", "make an HTTP -request", etc. +request", etc. This can work well in the beginning, but as your application grows +and the number of things you can do grows, naming messages as actions can become +problematic. + +In particular, it encourages you to recursively call your `update` function with +different messages when you want to compose behaviour. Gleam is a functional +programming language: we should use functions to update our state, not message +dispatching! Communicating through messages is a way for the _outside world_ to +talk to our application, not for our applications to talk to themselves. + +A recursive update function makes it difficult to see the consequences of any one +message as you need to trace through the recursive calls in your head to understand +which messages are being dispatched and in what order. + +It is important to note that on occasion, framing messages as actions _does_ make +more sense. In the [interactivity example](https://github.com/lustre-labs/lustre/tree/main/examples/02-interactivity) +we have messages like `Incr` and `Decr`. These messages don't get any clearer if +we opt for a name like `IncrButtonClicked` but as the examples grow in complexity, +such as the [HTTP example](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests), +message names tend to be more descriptive – `GotMessage` – and less action-oriented. ## View functions not components |