Use UI Foundation headlessly
Drive @tetherto/mdk-ui-foundation stores from any runtime without React
@tetherto/mdk-ui-foundation
@tetherto/mdk-ui-foundation is the framework-agnostic headless layer of the MDK App Toolkit. This how-to walks through installing it on its own and driving its Zustand stores from a non-React runtime — a Node script, a Vue or Svelte adapter you're authoring, a CLI tool, or a test helper.
When to reach for this
Use headless UI Foundation when:
- You're authoring a framework adapter (Vue, Svelte, Web Components) and need raw access to the Zustand stores.
- You're building a Node CLI or backend service that has to read MDK telemetry and act on it.
- You're writing test helpers or fixtures that need to seed and inspect store state without a React renderer.
- You need to subscribe to store changes from non-UI code — logging, websocket bridges, metrics.
For a React app, the React adapter wraps UI Foundation with <MdkProvider> and adapter hooks. Use that path instead so most React code never touches @tetherto/mdk-ui-foundation directly.
Install
@tetherto/mdk-ui-foundation has no peer dependencies on React or any UI framework.
npm install @tetherto/mdk-ui-foundationSubpath imports
Pull only the pieces you need from the relevant subpath. Subpath imports give tree-shakers a smaller surface than the top-level barrel:
import { authStore, devicesStore } from '@tetherto/mdk-ui-foundation/store'
import { createMdkQueryClient } from '@tetherto/mdk-ui-foundation/query'
import type { Rack } from '@tetherto/mdk-ui-foundation/types'These are the subpath entries most apps need. The package also publishes /app-shared-modules,
/app-ui-paths and /stores.json.
Create a QueryClient
createMdkQueryClient returns a TanStack Query Core client wired to your Gateway. Pass an explicit apiBaseUrl, or let the factory resolve one from environment variables:
import { createMdkQueryClient } from '@tetherto/mdk-ui-foundation/query'
const queryClient = createMdkQueryClient({
apiBaseUrl: 'https://gateway.example.com',
})Without an explicit apiBaseUrl, the factory checks VITE_MDK_API_URL, then MDK_API_URL, then the deprecated
VITE_API_BASE_URL / API_BASE_URL (which log a one-time warning), before falling back to
http://localhost:3000. Passing '' is not the same as passing nothing: it means "use relative URLs", which is
what you want when the Gateway serves the UI.
Bring your own backend
createMdkQueryClient accepts a fetcher, the seam for pointing the query layer at a different transport: a custom
auth scheme, a different protocol, or fixtures served from memory for a server-less demo.
const queryClient = createMdkQueryClient({
apiBaseUrl,
fetcher: myFetcher,
})The resolved base URL and that fetcher are stashed on the client's query and mutation meta, so everything reading
through the client picks them up.
[!NOTE] Earlier releases also shipped an
endpointsoption and a set of prebuilt query and mutation factories. Both were removed in 0.12.0. To read a route your Gateway app serves, call it withuseQuerydirectly.
Read store state
Each store is a Zustand vanilla singleton. getState() returns the current snapshot:
import { authStore } from '@tetherto/mdk-ui-foundation/store'
const { token, permissions } = authStore.getState()
console.log('current token', token)Write store state
setState() accepts either a partial object or a function that receives the previous state:
import { devicesStore } from '@tetherto/mdk-ui-foundation/store'
devicesStore.setState({ filterTags: ['rack-a'] })
devicesStore.setState((prev) => ({
selectedDevices: [...prev.selectedDevices, newDevice],
}))Subscribe to changes
subscribe() runs a callback on every state change and returns an unsubscribe function:
import { notificationStore } from '@tetherto/mdk-ui-foundation/store'
const unsubscribe = notificationStore.subscribe((state) => {
console.log('unread notifications:', state.count)
})
unsubscribe()A complete Node example
A small Node script that signs itself in with a token you already hold, reads a route your Gateway app serves,
and prints the unread notification count. A Gateway serves exactly the routes its declared apps provide, so
substitute your own path for /site/devices:
import { createMdkQueryClient } from '@tetherto/mdk-ui-foundation/query'
import {
authStore,
devicesStore,
notificationStore,
} from '@tetherto/mdk-ui-foundation/store'
const apiBaseUrl = process.env.MDK_API_URL ?? 'http://localhost:3000'
const main = async (): Promise<void> => {
const queryClient = createMdkQueryClient({ apiBaseUrl })
authStore.setState({ token: process.env.MDK_TOKEN ?? '' })
const devices = await queryClient.fetchQuery({
queryKey: ['devices', 'list'],
queryFn: async () => {
const res = await fetch(`${apiBaseUrl}/site/devices`, {
headers: { Authorization: `Bearer ${authStore.getState().token}` },
})
if (!res.ok) {
throw new Error(`ERR_DEVICES_REQUEST: ${res.status}`)
}
return res.json()
},
})
devicesStore.getState().setSelectedDevices(devices)
console.log(`Found ${devices.length} devices`)
console.log(`unread notifications: ${notificationStore.getState().count}`)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})Run it with:
MDK_TOKEN=ey... MDK_API_URL=https://gateway.example.com node script.tsFor the full list of query helpers, see the UI reference.
Next steps
- UI reference: full store list, query helpers, and the
createMdkQueryClientresolution order. - What's an app?: where the UI devkit fits into an MDK app's anatomy.
- React adapter: if you decide to layer React on top.