Managing State in Elm Single Page Applications
I've now built a few different applications in Elm - everything from simple event websites to more complex SAAS based applications. I've iterated multiple times on structuring single-page applications in Elm. My focus has been on managing state efficiently across pages, templates, and shared components that can be rendered across multiple pages.
I thought it'd be helpful to share the approach I've so far standardized on - and to do so I've built a very simple, plain web application demonstrating the various places where state is managed including:
- Simple pages that don't need a model nor any commands
- Stateful pages
- A global template that itself has state
In this application, we also demonstrate how to implement Login, Logout and profile updates which may then impact other components (e.g. changing your name in the application should immediately update the global template).
Hope you find this useful and if you have any suggestions, improvements or questions please do share!
Source Code & Live Demo
Source Code: GitHub Repo
Authentication
I've found that it has been helpful to handle authentication in Main.elm as this provides a single place to check state and enables us to create pages that document whether or not they require a user to be logged in.
My approach has been to define a type 'GlobalState' type which tracks authentication status:
Within main then, we need to first initialize the state and then route to the appropriate page. This also means that we can centralize the logic on when to ask the user to login or register directly from Main.
To handle initialization, we declare the Model in Main as either being in the init state (InitModel) or Ready (ReadyModel).
If we initialize the application with a session id - we will then load the session and upon completion continue with a ready model.
Example of redirecting the user to login as necessary:
The method pageAuthenticatedData will parse the global state and either redirect the user to login or else init PageRestricted, passing in the fully authenticated information.
Update
When invoking update from Main, we construct the arguments we need based on what the page requires. Possibilities include:
- The specific type of GlobalState the page requires
- A msgMap to convert page messages back into the Main Msg type. We'll need this when handling login or other events
- Variants to handle onLoggedIn, onLoggedOut, OnSessionUpdate which are essentially callbacks to Main when one of these events happen.
For pages that use the global template (called Shell in this application), we provide the Shell.ViewProps which contain the model for the Shell itself as well as a function to map messages in the template to the Main Msg type
The key design goals here are to:
- Minimize the boilerplate in Pages by providing only what the page needs
- Centralizing key State where it belongs - Main owns the Global State. Shell owns the Shell Model. All operations on this state have a single owner and the current state is passed to pages or components as needed.
As an example - when you increment the counter in the header, that message is handled by the ShellMsg defined in Main - and then updates are passed in explicitly to all pages that render the template. This is how we ensure state changes propagate immediately to all components.
Public Page
The page Content is available to all users. This page does not actually need any global data in any of the normal Elm functions (init, update, view) and thus uses a comment to specify that the page should be accessible by anonymous users:
Restricted Page
The page Restricted is available only to logged in users, but similarly does not access Global State in any of the standard elm functions. Thus we document that authentication is required with a comment:
Code Generation
In my own work and applications, I use a built code generators which works by:
- Find all files under src/Page
- Parse the init, update, and view functions to understand the arguments and return values
- Generates the code you see in Main.elm based on the type signatures of those functions
Broadly the goal is to fully standardize the inputs to the various pages to enable features like code generation which can be helpful as applications grow.
Thank you
A big thank you to Richard Feldman for his original work on an Elm Spa example and to Dwayne Crooks for his recent example Dwayne's Conduit
Have suggestions or comments? Please find me on X @mbryzek