Post "Elm at NoRedInk"

I’ve written a blurb about our experiences writing Elm at scale at NoRedInk.

Let me know if you have questions, I’ll be happy to clarify anything!

27 Likes

Really love that you wrote this!

I am curious about things like build times, how long it takes to test, or anything else like that, that you’re able to share.

1 Like

Gladly! Compiling Elm and running Elm tests is reasonably fast:

$ elm-test tests/
Compiling > Starting tests

elm-test 0.19.1-revision6
-------------------------

Running 4060 tests.


TEST RUN PASSED

Duration: 27597 ms
Passed:   4060
Failed:   0

The usual time hog is recompiling each entry point through Shake and esbuild. If you think that it takes around 1.5s for each entrypoint, it would take us 150 seconds for a change to a shared module that every page imports!

So @brian wrote a script that traverses the import tree and figures out which entry points need to be recompiled when you change a Elm module. So this is our day-to-day experience with making changes:

That’s fast enough for you to make a change, switch to the browser and refresh the page without having to think “oh I should wait for the compilation to finish”.

We have a similar script (also made by brian) that figures out the subset of tests to be executed when making a change, so that it’s easy to work in a TDD fashion.

4 Likes

We just finished moving all our bundling to esbuild. I can build all 106 entrypoints in something like a minute and a half on a 2016-era MBP. However, a full rebuild is rarely necessary, because our build tool (Shake) uses the import graph code as well, to track which entrypoints are stale.

6 Likes

Hi! Thanks for sharing some of your architectural choices.

I wonder why you decided to manage the main URL router with the backend. Was it your choice because it enabled you to have separately built Elm apps?

In our case we have “one” Elm app (an SPA if you will), served from a static hosting, that builds up of many embeddedable elements and therefore it acts as the main URL router.

Do you share code between your Elm apps? It’s clear that you have a UI library. Anything else?

You mentioned you fetch the initial data and pass it to your Elm apps via flags. Do you fetch just the minimum here or do you pass as much as you can already to save further requests? I guess when the apps need more data from the backend, you simply send XHR requests, right?

2 Likes

Does NRI apps have to contend with problems caused by browser or browser extensions being incompatible with Elm? How is it solved?

1 Like

Indeed! We used to have many react apps served over many Rails endpoints, so it helped us to port the whole service to Elm in a gradual fashion. It was also really useful to do big code changes, such as upgrading from 0.18 to 0.19, or switching our build system to use esbuild. In general, it makes small experiments cheap.

We do. Usually we tend to group code around either UI elements or domain entities. So for example we have a folder called Data where we expose the most commonly used data entities. Here’s a small example:

module Data.UserKind exposing (Kind(..), fromString)

type Kind
    = Student
    | Teacher

fromString : String -> Kind
fromString kind =
    if kind == "teacher" then
        Teacher

    else
        Student

This file lives in src/Data/UserKind.elm. We tend not to be too strict about where data lives, so sometimes you would find some domain entities which are only specified in the place where they’re being used. The general rule of thumb is "if you need this in more than two apps, move it to Data".

I don’t think there’s a hard and fast rule here. My usual preference is to use XHR requests where there would be ‘too much’ data to load, although how much data is too much is open to debate. Definitely loading the data via flags leads to a much simpler Elm application (no need to handle loading states, no need to handle failure states, no need to retry, etc.), so I think that comes into the decision as well.

Loads! :smiley: Unfortunately, browsers and browser extensions can break the web in all sorts of magical and unpredictable ways. If you are referring about extensions treating the DOM as a source of truth, I don’t think it’s particular about Elm, since other frameworks suffer similar issues. I don’t know of anything that us web developers could do, since at the end of the day, the code ends up running on the user’s infrastructure. What we can do is track the exceptions (we use Bugsnag) and see which percentage of our user base is affected by a specific problem. We also have lovely customer success folks that will dig deeper and come back to us with browser version, list of extensions, screenshots and videos of the users’ problems, which helps a lot when debugging.

3 Likes

Thanks for posting this! Very timely for me as I decided to try out using a backend to serve pages instead of going full elm SPA. Something I haven’t decided on is how much of the UI to keep in elm. For example, should I use backend templates for the static UI and sprinkle elm elements where interactivity is needed, or should I keep the backend pages as empty skeletons and put the entire UI in elm? There are different trade-offs with each method so I was wondering how you decided on and handled your implementation in this regard?

I’d love to learn more insight into these decisions as well from the perspective of such a large application.

Templating as much as possible server side is the quickest way to deliver most apps/websites imo.

Sprinkle Elm in for things more complex than just a basic form, like multistep forms, calendars, maybe a carousel, trello-like cards, etc. This way you also get a quick loading site without the js bloat

Hey! Great questions. We started small by adding Elm components to our Rails and React pages, and over time, it took control of all the page. The advantage of that is that we can ensure that the same UI components are reused across the whole user experience. In most cases, I would recommend starting small and only adding Elm where you think it would add the most value: complex UI interactions, complex data structures, things you would be scared to write in JavaScript.

In our use case, we have many engineers (~20), so having Elm control the whole page is great. Imagine you have a pull request that has been open a couple of days, and in the meanwhile the main branch has changed various UI components. The moment I merge master in my branch I’m forced to deal with all the inconsistencies immediately, without having to wait for the whole test suite to run to let me know that something is broken. The feedback loop resolves in seconds, rather than multiple minutes.

3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.