Remote / foldkit-remote

RemoteData: what does the Model know right now?

A Remote Projection never lies by pretending an absent value is present. It returns a closed RemoteData union:

                         no active fetch
missing ------------------------------------------------> Initial

                         fetch starts
missing ------------------------------------------------> Loading
                                                            |
                                                         result
                                                            v
                                                          Ready
                                                            |
                                                       refetch starts
                                                            v
                                                        Refreshing
                                                        old value stays
                                                            |
                                                         result
                                                            v
                                                          Ready

server says entity is absent ---------------------------> NotFound
stored value fails Selection decoding -----------------> Failed
a read or a list's query fails ------------------------> Failed (with the old value, if any)

The states are:

  • Initial — required data is absent and nothing is currently fetching it.
  • Loading — required data is absent and a read, or a list's query, is in flight.
  • Ready — every selected field is present and decodes.
  • Refreshing — the current value remains visible while it is being refetched.
  • Failed — stored server data does not decode against the Selection, the read or query behind it failed, or the server settled a selected field without a value (an Unavailable error; see When a field is withheld). A value that was already shown is kept as previous.
  • NotFound — the entity is represented by a tombstone: a live event deleted it, or the server was asked for it by id and answered with nothing about it. That id is then known absent, whether it never existed, is gone, or is hidden whole from this principal. It is not planned again until Data.refresh forces it or a write brings it back. The target of a returned ref is not marked this way: a server need not expand a relation that rides on a request, and the planner asks for such a target by id next, which is when its absence is learned.

Initial is intentionally different from Loading. A Projection belonging to no active Surface may remain Initial forever. Rendering a spinner for Initial can therefore hide an activation/wiring mistake; Loading is the state that actually means "wait for this request."

When a read sits at Initial and you expected data, ask the Model why with Data.why; see Why is it still Initial?.

RemoteData.match is exhaustive, so adding or omitting a state is visible at compile time. RemoteData.answered(data) is whether a read has an answer yet: Ready, Refreshing, Failed or NotFound, not Initial or Loading.

Drawing one: RemoteData.render

Most views draw these six states three ways, under one policy: useful data stays on screen. RemoteData.render is that fold.

ts
RemoteData.render(project, {
  loading: () => ProjectSkeleton(),
  notFound: () => NoSuchProject(),
  failed: error => ErrorView(error),
  data: (value, freshness) =>
    ProjectView({ project: value, dimmed: freshness._tag !== 'Fresh' }),
})

Initial and Loading reach loading; Ready and Refreshing reach data; a Failed that still carries the value it had reaches data too, so a read that failed does not throw away what the reader was already looking at. Only a Failed with nothing to show reaches failed.

The data branch is told which it got, as a Freshness — exported, so a view helper that takes one can name it:

FreshnessWhat it means
FreshThis is the current answer.
RefreshingA newer answer is on its way; this one is still good.
StaleA read failed; this is what was last known good, with its error.

It is one tag rather than a pair of booleans because a value cannot be both refreshing and stale, and a type that can say so invites a view to handle a state that never arrives.

notFound is its own branch and not optional. A row the server answered for and does not have is neither loading nor a failure; drawing it as either is a spinner that never ends or an error nobody can act on.

Reach for match instead when the six states really do draw differently — it stays the exhaustive fold, and render does not replace it.