Mirror / foldkit-mirror

Sixty seconds: put a filter in the URL

Start from a normal Surface application:

ts
import { Match, Schema } from 'effect'
import { defineMessageUnion } from 'foldkit/message'
import { Url } from 'foldkit/url'
import { Surface } from 'foldkit-surface'
import { Mirror } from 'foldkit-mirror'

const Model = Schema.Struct({
  filter: Schema.Literals(['all', 'active', 'done']),
  page: Schema.Number,
})
type Model = typeof Model.Type

const Message = defineMessageUnion({
  UrlChanged: { url: Url },
})
type Message = typeof Message.Type

const initial: Model = {
  filter: 'all',
  page: 1,
}

const App = Surface.application({
  Model,
  Message,
  initial,
  update,
})

Declare which local fields the URL represents:

ts
const Filters = Mirror.url(App, {
  name: 'filters',
  fields: [
    App.model.filter,
    App.model.page,
  ],
})

With the default values, the URL stays clean. If the Model becomes:

ts
{
  filter: 'active',
  page: 2,
}

the mirror represents that as approximately:

?filter=active&page=2

The field names, default values, and basic text codecs come from the application fields; they are not declared again.

Navigation comes back through the application's normal URL Message:

ts
function update(model: Model, message: Message) {
  return Match.value(message).pipe(
    Match.tag('UrlChanged', ({ url }) => ({ model: Filters.reduce(model, url) })),
    Match.orElse(() => ({ model })),
  )
}

And the mirror's Subscription writes Model changes outward:

ts
import * as Subscription from 'foldkit/subscription'

const subscriptions = Subscription.make<Model, Message>()(() => ({
  ...Filters.subscriptions,
}))

At runtime, also reduce the initial URL before the first render and map later navigation into Message.UrlChanged. The Subscription writes outward; it does not replace those inbound steps. Mirror.routing (next section) wires both into Runtime.makeApplication; with foldkit-bundle, Filters.wiring('UrlChanged') and wiring.url(...) do (see "One list per application with wiring").

That is the entire URL loop:

Model -> subscription -> URL
URL -> UrlChanged -> reduce -> Model

The loop closes without a second render: the mirror's own write comes back as a UrlChanged whose keys hold what the Model already holds, and reduce then returns the Model it was given. It does the same for a navigation that leaves its keys alone, and Prefs.reduce for a restore that finds nothing new.