Mirror / foldkit-mirror
Sixty seconds: put a filter in the URL
Start from a normal Surface application:
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:
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:
{
filter: 'active',
page: 2,
}the mirror represents that as approximately:
?filter=active&page=2The 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:
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:
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 -> ModelThe 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.