Effect
Overview
The Effect msg
type in Elm Land is an abstraction built on top of Elm's standard Cmd msg
type. In this guide, we'll learn:
- How to use the default
Effect
module - How to customize the
Effect
module - The benefits of defining custom effects for your application
Effect
By default, Elm Land has a few effects defined for you. Here's the module's API, and what each function does:
module Effect exposing
( Effect
, none, batch
, sendCmd, sendMsg
, pushRoute, replaceRoute, loadExternalUrl
)
Effect.none
Similar to Cmd.none, this tells Elm Land not to run any side-effects.
Definition
Effect.none : Effect msg
Effect.batch
Similar to Cmd.batch, this allows you to send many effects at once.
Definition
Effect.batch : List (Effect msg) -> Effect msg
Effect.sendCmd
Convert a Cmd
to an Effect
.
Definition
Effect.sendCmd : Cmd msg -> Effect msg
"Wait... should I be using this?"
The Effect.sendCmd
is great for first learning the Elm Land framework, or if you're tinkering around. For production applications, we recommend that you prefer defining custom effects.
Later in this guide, you'll learn how "custom effects" use Cmd
behind-the-scenes, to help make your life easier when working in pages or layouts.
Effect.sendMsg
Send a msg
as an Effect
. This is perfect for "stateful components" that need to emit events up to their parent.
Definition
Effect.sendMsg : msg -> Effect msg
Effect.pushRoute
Push a new URL onto the browser history. This is just like Browser.Navigation.pushUrl, except it doesn't require a Key
argument.
Definition
Effect.pushRoute :
{ path : Route.Path.Path
, query : Dict String String
, hash : Maybe String
}
-> Effect msg
Effect.pushRoutePath
Push a new Path onto the browser history as a route without query and hash. This is just like Browser.Navigation.pushUrl, except it doesn't require a Key
argument.
Definition
Effect.pushRoutePath : Route.Path.Path -> Effect msg
Effect.replaceRoute
Replace the current browser history entry with a new URL. This is just like Browser.Navigation.replaceUrl, except it doesn't require a Key
argument.
Definition
Effect.replaceRoute :
{ path : Route.Path.Path
, query : Dict String String
, hash : Maybe String
}
-> Effect msg
Effect.replaceRoutePath
Replace the current browser history entry with a new Path as URL without query and hash. This is just like Browser.Navigation.replaceUrl, except it doesn't require a Key
argument.
Definition
Effect.replaceRoutePath : Route.Path.Path -> Effect msg
Effect.loadExternalUrl
Navigate to an external URL, outside your application. This is just like Browser.Navigation.load, except it returns an Effect
rather than a Cmd
.
Effect.loadExternalUrl : String -> Effect msg
Effect.back
Navigate back on page. This is just like Browser.Navigation.back, except it returns an Effect
rather than a Cmd
.
Effect.back : Effect msg
Comparison with Cmd
In the official Elm guide, we learned that Cmd Msg
is the way that Elm applications can send "side-effects". The core packages and Elm package ecosystem use this type to send HTTP requests, generate random numbers, and more.
-- ┏━ The state of our page
-- ┃
( Model, Cmd Msg )
-- ┃
-- ┗━ Literally everything else
In Elm, the Cmd
type is the lowest level primitive for creating side-effects. In Elm Land, the Effect
type is an abstraction on top of Cmd
. As we'll learn in the next section, they allow us to create custom tailored commands specific to our application's needs.
-- ┏━ The state of our page
-- ┃
( Model, Effect Msg )
-- ┃
-- ┗━ Literally everything else
Using commands in practice
Let's imagine we were building a Twitter clone. When our homepage loads, we want to fetch posts for the feed.
If we did this with Elm commands, the API calling code for fetching that feed would look something like this:
module Pages.Home_ exposing (Model, Msg, page)
-- ...
page : Shared.Model -> Route () -> Page Model Msg
page shared route =
Page.element
{ init = init shared
, ...
}
-- ...
init : Shared.Model -> () -> ( Model, Cmd Msg )
init shared _ =
( { model | posts = Loading }
, Http.request
{ method = "GET"
, url = shared.baseApiUrl ++ "/api/feed"
, headers =
case shared.user of
Just user ->
[ Http.header "Authorization" ("Bearer " ++ user.token)
]
Nothing ->
[]
, body = Http.emptyBody
, expect = Http.expectJson GotPostsForFeed decoder
, timeout = Just 15000
, tracker = Nothing
}
)
decoder : Json.Decode.Decoder (List Post)
decoder =
...
-- ...
I've highlighted a few things to note from the snippet above:
- We need to pass in the
shared
value toinit
, so the HTTP request can access certain variablesshared.baseUrl
– It's common to requesthttp://localhost:3000/api
in development, buthttps://api.myapp.com
in production. The.baseApiUrl
field is based on an environment variable, and ensures we use the right endpoint in dev and productionshared.user
– We also conditionally apply anAuthorization
header if a user is signed in. This makes our API return posts based on a user's followers. For a signed-out user, we just show the popular stuff.
- Some web applications will want to enforce other things, like a 15 second
timeout
before terminating the request
You can imagine a lot of this code will need to be repeated as we add more features. For example, if we wanted to create a new post with a POST
request later, we'd need to pass around shared.user
all over again.
Functions definitely can help reduce the boilerplate, but those functions would still need access to the shared
value to work.
Using effects in practice
Let's do the same request, but with Elm Land's Effect
abstraction:
-- ...
init : () -> ( Model, Effect Msg )
init _ =
( { model | posts = Loading }
, Effect.sendApiRequest
{ endpoint = "/api/feed"
, decoder = decoder
, onResponse = GotPostsForFeed
}
)
-- ...
Effects let us talk about our side-effects at a higher level. They allow us to:
- Prevent the need to pass
shared
to everyinit
orupdate
functions that sends an API request - Prevent bugs and other surprises that come from forgetting to correctly wire up values like
headers
ortimeout
- Create end-to-end tests for our application, using elm-program-test
Custom effects
Using the elm-land customize
command, we can eject the default Effect
module into src/Effect.elm
.
elm-land customize effect
From there, you'll have complete control over the Effect
module!
Define your ports here!
In Elm Land, the convention is to use the Effect
module for any ports. We recommend defining one incoming and one outgoing port in this module.
From there, expose small functions like Effect.saveUser
and Effect.clearUser
to avoid dealing with JSON encoding elsewhere in your application! See the User Auth guide for a practical example on doing this with local storage.
Example 1: Shared.Msg
If you've customized the Shared module, you may also want to send Shared.Msg
values from a page, like Shared.Msg.SignOut
. Effects can send commands, but they can also send Shared.Msg
under the hood.
Here's what you would need to add to support Effect.signOut
on any page or layout:
module Effect exposing
( Effect, none, batch
, ...
, signOut
)
-- ...
signOut : Effect msg
signOut =
SendSharedMsg Shared.Msg.SignOut
-- ...
For convenience, the SendSharedMsg
variant is already defined within the Effect
module. It's really that easy!
Note: Rather than exposing one Effect.sendSharedMsg
function, we recommend only exposing the effects you'll need. This will make each Effect
easier to use, and help you easily see which Shared.Msg
values are actually called.
Here's a visual of how it will help provide a nicer API in practice:
-- ❌ DON'T – Expose a generic `sendSharedMsg`
Effect.sendSharedMsg
(Shared.Msg.SignIn
{ email = model.email
, password = model.password
}
)
-- ✅ DO – Expose simple functions as needed
Effect.signIn
{ email = model.email
, password = model.password
}
Example 2: HTTP requests
Earlier in this guide, we showed an example Effect.sendApiRequest
. Let's walk through a quick visual example of how to implement that function under the hood:
module Effect exposing
( Effect, none, batch
, ...
, sendApiRequest
)
-- ...
type Effect msg
= ...
| SendApiRequest
{ endpoint : String
, decoder : Json.Decode.Decoder msg
, onHttpError : Http.Error -> msg
}
-- ...
sendApiRequest :
{ endpoint : String
, decoder : Json.Decode.Decoder value
, onResponse : Result Http.Error value -> msg
}
-> Effect msg
sendApiRequest options =
let
decoder : Json.Decode.Decoder msg
decoder =
options.decoder
|> Json.Decode.map Ok
|> Json.Decode.map options.onResponse
onHttpError : Http.Error -> msg
onHttpError httpError =
options.onResponse (Err httpError)
in
SendApiRequest
{ endpoint = options.endpoint
, decoder = decoder
, onHttpError = onHttpError
}
-- ...
map : (msg1 -> msg2) -> Effect msg1 -> Effect msg2
map fn effect =
case effect of
...
SendApiRequest data ->
SendApiRequest
{ endpoint = data.endpoint
, decoder = Json.Decode.map fn data.decoder
, onHttpError = \err -> fn (data.onHttpError err)
}
-- ...
toCmd :
{ key : Browser.Navigation.Key
, url : Url
, shared : Shared.Model.Model
, fromSharedMsg : Shared.Msg.Msg -> msg
, batch : List msg -> msg
, toCmd : msg -> Cmd msg
}
-> Effect msg
-> Cmd msg
toCmd options effect =
case effect of
...
SendApiRequest data ->
Http.request
{ method = "GET"
, url = options.shared.baseApiUrl ++ data.endpoint
, headers =
case options.shared.user of
Just user ->
[ Http.header
"Authorization"
("Bearer " ++ user.token)
]
Nothing ->
[]
, body = Http.emptyBody
, expect =
Http.expectJson
(\httpResult ->
case httpResult of
Ok msg ->
msg
Err httpError ->
data.onHttpError httpError
)
data.decoder
, timeout = Just 15000
, tracker = Nothing
}
We do some fancy Json.Decode.map
stuff in our function to avoid needing the generic value
type variable for our Effect msg
type.
Although there's quite a bit of logic up front, you'll only define this effect once per application. The time savings come from actually making HTTP requests throughout the application, and not wiring up all this stuff each time.
The benefits become much clearer in the official "Error Reporting" example. There, we add some extra logic to ensure that all JSON decoding errors are automatically logged to Sentry, to help us debug issues that come from unexpected API responses.