This is a small demo that explores different approaches for creating a potentially scalable single page application. See here for the completely unspectacular sample apps in action, and here for my conclusion.
- There is a global session object that potentially can hold shared state for all parts of the application.
- The login page authenticates the user and initates the creation of the session.
- After logging in, the user is redirected to the dashboard, where they are welcomed by name.
- There is an additional profile page, showing the name again.
- From the profile page it's possible to go to an "edit profile" page that allows changing that name.
- After the name has changed, that change must be reflected in all parts of the application: dashboard, and profile page.
- Although "edit profile" could be seen as a child of "profile", it is kept on the same level for simplicity reasons (we potentially already are at the third level here).
That scope of functionality allows us to look at the following questions:
- How are parent-child relationships handled?
- How much boilerplate code needs to be written?
- How much guidance and support provides the compiler?
- How maintainable, aka scalable, does the approach appear to be?
Elmish + Feliz
- When introducing intents (aka external messages), the parent doesn't know about them until we start handling them there. So the compiler wouldn't complain if we forget to add it to the parent(s).
- Routing can fail silently when the expected route isn't found. Sometimes that's just a matter of lower and upper case letters.
Feliz.Routeris best being used with arrays representing the url segments, because that's what can be passed to the
format()function (it does not accept lists).
- The Feliz syntax felt rather verbose at first. Unfortunately,
Htmlis a type and not a module and therefore "cannot be opened". However, in direct comparison to the "classic" Elmish view syntax, for real world tasks there seems not to be that much of a difference in terms of the number of written lines of code. Good old HTML will in most cases still stay shorter, though...
- Except for commands fully testable, as everything is (mostly pure) F# functions
- Does not require much knowledge of React
React Function Components + Feliz
- Much less boilerplate code to write in comparison to Elmish + Feliz
- It is basically React written with F#, so React knowledge is needed
- In addition, regular updates are needed whenever React changes
- Working with the context API of react needs getting used to, but is then relatively straightforward
- Passing data to components through props is straightforward as well
useElmishis useful for more complex forms (see login and edit profile), but simple operations can also be handled more pragmatically (see logout)
- Right now, Hot Module Reloading (or Fast Refresh) does not work. Which is a clear obstacle during development time.
- TBD: Testability
React + TypeScript
- Having not actively worked with TypeScript since its early days in 2012, it was surprisingly easy to get started.
- Although this small demo doesn't use much of the language's features, those that are being used were really easy to pick up and a pleasure to work with.
- The tooling based on Visual Studio Code is amazing. I not only like the automatic code-formatting but also the really fast feedback loop that is provided during development time.
- When the build breaks, it does so within milliseconds, and error messages are always helpful. Most of the time, however, it already becomes obvious that something went wrong while typing.
- I could take over all the concepts (components, context, ...) from the previous implementation with Reaction Function Components + Feliz, which allowed me to implement the TypeScript demo within a couple of hours.
- I decided to implement the two parts of the app which contain a bit more "logic" (login + edit user name) without following any strict pattern like MVU. As everything is self-contained in its own component, this seems reasonable.
- react-router is powerful but relatively easy to get started with.
- As with the Feliz sample, function components are easy to reason about.
Lines of Code
|Function Components + Feliz||13||59||6||316|
|Elmish + Feliz||11||77||6||404|
|React + TypeScript||11||27||1||241|
Performance and bundle size
The following is measured by looking at the final and production-ready compile SPAs with Google Chrome (83) on macOS. It is obviously not objective and may be improved by some settings (I just used the default configuration as provided by the templates I used). But it gives an idea about what to expect.
|Function Components + Feliz||8||253 KB||466 KB||167 ms||166 ms|
|Elmish + Feliz||7||253 KB||468 KB||171 ms||173 ms|
|React + TypeScript||7||216 KB||328 KB||131 ms||132 ms|
(Transferred = compressed, Resources = uncompressed; in each case the fastest run was chosen)
- Elmish Parent-child composition
- Elm Shared State example
- Design of Large Elm apps
- Pros/cons of Elmish vs plain React components (via Fable.React)
- Child-Parent Communication in Elm: OutMsg vs Translator vs NoMap Patterns
- TypeScript docs
- React Router
- React+TypeScript Cheatsheets
- Create React App