Elm’s compiler is a great assistant. We can make HUGE changes to our code and it will make sure we make all the necessary fixes to get it working again. It gives us complete freedom to refactor.
Just because we can doesn’t mean we should. I’ve found it’s often much nicer to take a lot of small steps, each of which leave us in a compiling state. Recently, I’ve been using incremental approach a lot for interactions involving HTTP requests, especially complex GraphQL queries.
In particular, faking out HTTP requests with a sleep allows me to hand-wave the request away while I deal with the various UI states (loading, success, failure), all while remaining in a compiling state.
Setup
We’ve introduced a button. When we click it we want to fetch a user from the API via HTTP.
button [ onClick UserClickedGetUser ]
[ text "Get the user" ]
Now we’re getting a compilation error because there’s a new message we aren’t handling. ❌
Do nothing – inline
We add a new case to our update
function. The simplest thing we can do is not
to make the HTTP request. We set the model into the loading state and return
Cmd.none
. Now we’re compiling again! ✅
We should be able to click the button and see the UI go into a loading state.
-- In the `update` function
UserClickedGetUser ->
({ model | user = Loading }, Cmd.none)
Do nothing – extract method
The next simplest thing we can do is to extract method. We’re still not doing anything, but now it’s happening elsewhere. We’re still compiling. ✅
-- In the `update` function
UserClickedGetUser ->
({ model | User = Loading }, getUser)
getUser : Cmd Msg
getUser =
Cmd.none
Sleep
Time to actually do some work in the command. In an attempt to tighten the
incremental steps we need to get to a compiling state, we’d like to skip making
an actual HTTP request for now. Let’s do something simpler instead. We can use a
combination of Process.sleep
and Task.perform
to create a command that
returns the same kind of responses that an HTTP request would.
getUser : Cmd Msg
getUser =
Process.sleep 2000
|> Task.perform (\_ ->
ReceivedUserFromServer (Ok fakeUser)
)
fakeUser : User
fakeUser =
{ name = "Fake", age = 42 }
Now the build is broken because we aren’t handling the new message
ReceivedUserFromServer
. ❌
Handle Result
Back in our update
function, we add branches for both success and failure
cases of our message. We probably also need to handle success and errors views
for those states. Now we’re compiling again! ✅
We should now be able to click the button, see a loading state, then after a couple of seconds see a success or error state. All without making any HTTP requests!
-- In the `update` function
UserClickedGetUser ->
({ model | user = Loading }, getUser)
ReceivedUserFromServer (Ok user) ->
-- HANDLE SUCCESS
ReceivedUserFromServer (Err error) ->
-- HANDLE ERROR
Real HTTP
Now that we’ve got all the UI interactions done, we can finally circle back to the HTTP request. We don’t need to worry about loading or error states. Those are already handled. All we need to do here is make the API call. Once that compiles, we’re done! ✅
getUser : Cmd Msg
getUser =
-- DO REAL HTTP HERE
See it in action
I’ve packaged the example above into a live Ellie demo.
Conclusion
I’ve found a lot of benefits to the incremental approach. Not only does it help focus my development process, but it also allows me to scope commits more tightly. I can actually run the app and interact with the HTML at almost any point in the development process.
As a bonus, this approach also allows me to develop against an API endpoint that doesn’t exist yet. This isn’t uncommon when working with a separate backend team that’s building features in parallel.
So the next time you’re integrating some API, throw a few sleeps into your code. You may find it’s a winning combo 🥇