United Kingdom: +44 (0)208 088 8978

Dealing with remote data using SAFE.Client

In this blog post, we look at some tools for dealing with remote data that are included in the SAFE.Client Package

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!