MDK Logo

Use UI Core headlessly

Drive @tetherto/mdk-ui-core stores from any runtime without React

@tetherto/mdk-ui-core

@tetherto/mdk-ui-core 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 Core 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 Core with <MdkProvider> and adapter hooks. Use that path instead so most React code never touches @tetherto/mdk-ui-core directly.

Install

@tetherto/mdk-ui-core has no peer dependencies on React or any UI framework.

npm install @tetherto/mdk-ui-core

Subpath 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-core/store'
import { createMdkQueryClient } from '@tetherto/mdk-ui-core/query'
import type { Device } from '@tetherto/mdk-ui-core/types'

The subpath exports table on the reference lists every supported entry.

Create a QueryClient

createMdkQueryClient returns a TanStack Query Core client wired to your App Node. Pass an explicit baseUrl, or let the factory resolve one from environment variables:

import { createMdkQueryClient } from '@tetherto/mdk-ui-core/query'

const queryClient = createMdkQueryClient({
  baseUrl: 'https://app-node.example.com',
})

Without an explicit baseUrl, the factory checks VITE_MDK_API_URL then MDK_API_URL before falling back to http://localhost:3000. The resolution order on the reference covers every case.

Read store state

Each store is a Zustand vanilla singleton. getState() returns the current snapshot:

import { authStore } from '@tetherto/mdk-ui-core/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-core/store'

devicesStore.setState({ selectedDeviceId: 'wm-002' })

devicesStore.setState((prev) => ({
  devices: [...prev.devices, newDevice],
}))

Subscribe to changes

subscribe() runs a callback on every state change and returns an unsubscribe function:

import { notificationStore } from '@tetherto/mdk-ui-core/store'

const unsubscribe = notificationStore.subscribe((state) => {
  console.log('unread notifications:', state.count)
})

unsubscribe()

A complete Node example

A small Node script that authenticates against the App Node, fetches the device list once, and then tails unread notification count changes:

import { createMdkQueryClient } from '@tetherto/mdk-ui-core/query'
import {
  authStore,
  devicesStore,
  notificationStore,
} from '@tetherto/mdk-ui-core/store'

async function main() {
  const queryClient = createMdkQueryClient({
    baseUrl: process.env.MDK_API_URL ?? 'http://localhost:3000',
  })

  authStore.setState({ token: process.env.MDK_TOKEN ?? '' })

  const devices = await queryClient.fetchQuery({
    queryKey: ['devices', 'list'],
    queryFn: async () => {
      const res = await fetch(`${process.env.MDK_API_URL}/api/devices`, {
        headers: { Authorization: `Bearer ${authStore.getState().token}` },
      })
      return res.json()
    },
  })
  devicesStore.setState({ devices })

  console.log(`Found ${devices.length} devices`)

  const unsubscribe = notificationStore.subscribe((state) => {
    console.log(`unread notifications: ${state.count}`)
  })

  process.on('SIGINT', () => {
    unsubscribe()
    process.exit(0)
  })
}

main().catch((err) => {
  console.error(err)
  process.exit(1)
})

Run it with:

MDK_TOKEN=ey... MDK_API_URL=https://app-node.example.com node script.ts

For the prebuilt query and mutation factories (authQuery, devicesQuery, deviceQuery, telemetryQuery), check the QueryClient factory section on the reference.

Next steps

On this page