Managing data coming from a remote location can be tricky, especially if it can be refreshed or replaced. That's why, inspired by the Elmish book, the SAFE.Client package includes some utilities to deal with that. In this blog post, I give a short overview of 2 types that help you deal with remote data in your app.
Most examples in this article come straight out of the SAFE template, which includes the SAFE.Client package. New to SAFE? The docs will get you started with it in a breeze!
The ApiCall type
The first of this mighty pair is the ApiCall
type. It's a generic type that takes two arguments; the first one is the input of the api call; the second one is the expected result. The ApiCall
type is used as the content of a Elmish message.
type ApiCall<'TStart, 'TFinished> =
| Start of 'TStart
| Finished of 'TFinished
Let's look at the SaveTodo
message in the SAFE.Template example; when calling the API with a string
, we expect it to return a Todo
.
type Msg =
...
| SaveTodo of ApiCall<string, Todo>
To dispatch this message, we wrap the string
in a Start
object, and in turn wrap that into the SaveTodo
message:
Html.button [
...
prop.onClick (fun _ -> dispatch (SaveTodo(Start model.Input)))
]
The Finished
message is generally called by the Cmd.ofAsync.perform
function, indicating that the api call is complete:
Cmd.OfAsync.perform todosApi.addTodo todo (Finished >> SaveTodo)
Now we just need to handle the messages. A nice advantage of this type is that we can deal with both forms of the ApiCall
message in the same place, making it clear that they belong together.
let update msg model =
match msg with
| SetInput value -> ...
| LoadTodos msg -> ...
| SaveTodo msg ->
match msg with
| Start todoText ->
let saveTodoCmd =
let todo = Todo.create todoText
Cmd.OfAsync.perform todosApi.addTodo todo (Finished >> SaveTodo)
{ model with Input = "" }, saveTodoCmd
| Finished todo ->
{
model with
Todos = model.Todos |> RemoteData.map (fun todos -> todos @ [ todo ])
},
Cmd.none
The RemoteData type
Where ApiCall
helps us bring loadable data into the page, RemoteData
helps us to represent this data in our model.
It's a discriminated union with 3 cases, representing various stages the data can be in:
type RemoteData<'T> =
| NotStarted
| Loading of 'T option
| Loaded of 'T
Generally, we tend to initialize a RemoteData
option as NotStarted
.
let initialModel = { Todos = NotStarted; Input = "" }
When we start loading data, we want to set Todos
to Loading
. There's nothing preventing you from setting it directly, but there is also the function RemoteData.startLoading
, that creates an updated copy for you.
The benefit of using startLoading
is that it works well when refreshing existing data. It saves the existing data, which means that during the refresh, there is no period without data.
Here's a couple of example transitions using startLoading
:
NotStarted |> RemoteData.startLoading // Loading None
Loaded x |> RemoteData.startLoading // Loading (Some x)
Loading (Some x) |> RemoteData.startLoading // Loading (Some x)
Loading None |> RemoteData.startLoading // Loading None
You can see an example of how to use this in the SAFE template when loading the todos using the now familiar RemoteData
message:
let update msg model =
...
| LoadTodos msg ->
match msg with
| Start() ->
let loadTodosCmd = Cmd.OfAsync.perform todosApi.getTodos () (Finished >> LoadTodos)
{ model with Todos = model.Todos.StartLoading() }, loadTodosCmd
...
Setting the data after loading is as simple as wrapping the result in the Loaded
case:
let update msg model =
...
| LoadTodos msg ->
match msg with
...
| Finished todos ->
{
model with
Todos = RemoteData.Loaded todos
},
Cmd.none
The RemoteData
type comes with a lot of convenient member functions, such as DefaultValue
, which returns either the value wrapped inside or a default value.
Another handy one is IsStillLoading
, which can be used to check if data is being loaded, regardless of whether data is already available. This is again useful when refreshing existing data: you may still want to display the existing data, but in the meantime indicate that a refresh is in flight.
To see a full list of the available attributes, check out the well-documented source code.
As you can see, the SAFE.Client
package has some very convenient types to aid you in dealing with remote data. The easiest way to use the package is by creating an app from the SAFE template, but you can also just install it as part of any other app!