Skip to content

Managing Tags

When creating Tags, it’s easy to slip into one of two extremes.

One Tag per Task — every shared value becomes a duplicate.

const dashboardTag = tag<{
user: User
widgets: Widget[]
}>("dashboard")
const dashboard = createTask({
name: "dashboard",
run: { context: dashboardTag.value, fn: () => {} },
})
const profileTag = tag<{
user: User
settings: Setting[]
}>("profile")
const profile = createTask({
name: "profile",
run: { context: profileTag.value, fn: () => {} },
})

One Tag per value — every new value means a new Tag and a new wire.

const dashboardUser = tag<User>("dashboard::user")
const dashboardWidgets = tag<Widget[]>("dashboard::widgets")
const dashboard = createTask({
name: "dashboard",
run: {
context: { user: dashboardUser.value, widgets: dashboardWidgets.value },
fn: () => {},
},
})
const profileUser = tag<User>("profile::user")
const profileSettings = tag<Setting[]>("profile::settings")
const profile = createTask({
name: "profile",
run: {
context: { user: profileUser.value, settings: profileSettings.value },
fn: () => {},
},
})

The right count sits between them.

The hybrid: one shared Tag, separate Tags for what differs.

import {
compose, createTask, createWire, tag, literal
} from "@app-compose/core"
const userTag = tag<User>("user")
const dashboardTag = tag<{ widgets: Widget[] }>("dashboard")
const dashboard = createTask({
name: "dashboard",
run: {
context: {
user: userTag.value,
widgets: dashboardTag.value.widgets,
},
fn: console.log,
},
})
const profileTag = tag<{ settings: Setting[] }>("profile")
const profile = createTask({
name: "profile",
run: {
context: {
user: userTag.value,
settings: profileTag.value.settings,
},
fn: console.log,
},
})
compose()
.step([
createWire({ from: literal(1), to: userTag }),
createWire({ from: { widgets: literal([]) }, to: dashboardTag }),
createWire({ from: { settings: literal([]) }, to: profileTag }),
])
.step([dashboard, profile])
.run()
type User = unknown
type Widget = unknown
type Setting = unknown

When a Task’s context type is deeply nested, a Tag often carries the whole type minus a few internal fields. Writing that subtype by hand is painful — and it goes stale the moment the source changes. Use OmitDeep from type-fest.

import type { OmitDeep } from "type-fest"
type DashboardCtx = {
user: { id: string }
navigation: {
sidebarApi: SidebarApi
title: string
}
widgets: Widgets[]
}
const dashboardTag = tag<OmitDeep<DashboardCtx, "navigation.sidebarApi">>("dashboard")
// same as { user: { id: string }; navigation: { title: string }; widgets: Widgets[] }