@tetherto/mdk-react-adapter
v0.13.1
Published
React framework adapter for @tetherto/mdk-ui-foundation. Binds Zustand vanilla stores and TanStack QueryClient to React-native hooks. No Redux.
Readme
@tetherto/mdk-react-adapter
React framework adapter for @tetherto/mdk-ui-foundation. Binds
the Zustand vanilla stores and the TanStack QueryClient from the
headless core into React-native hooks.
Install
npm install @tetherto/mdk-react-adapter[!NOTE]
reactandreact-dom19 are peer dependencies.
Surface
<MdkProvider apiBaseUrl={...}>— wrapsQueryClientProviderfrom@tanstack/react-queryand exposes the resolved API base URL via React context. Required at the app root.- Store hooks:
useAuth,useDevices,useTimezone,useNotifications— one per core store, implemented withuseStore(<vanillaStore>)fromzustand. - Utility hooks: permissions (
useHasPerms,useCheckPerm,useIsFeatureEditingEnabled), auth token lifecycle (useAuthToken,useTokenPolling), timezone formatting (useTimezoneFormatter), and UI helpers (usePagination,useLocalStorage,useKeyDown,useWindowSize,useDeviceResolution,usePlatform,useBeepSound,useContextualModal,useSubtractedTime,useGetAvailableDevices). - Report state:
usePeriod, a reporting period kept in the URL, andtoQueryView, a query's state as loading, error or ready. - Installed-app hooks:
useInstalledApps,useInstalledAppUis,useInstalledAppPanels. - App UI modules:
defineMdkApp({ id, displayName, routes, panels, sections }), the contract a shell loads. A route names itssectionto group its sidebar entries; an entry insectionsgives that section an icon, and a section works without one. Field table: the apps guide. - Re-exports of
useQuery,useMutation,useQueryClientfrom@tanstack/react-queryso consumers can stay on a single import path.
Authentication
<MdkProvider auth={...}> takes an AuthProvider (from @tetherto/mdk-ui-foundation) —
the seam useMdkAuth() reads back. Four are available:
noAuth()— no token, no sign-in, nothing to expire; for a fixture-backed demobearerTokenAuth()— bearer token inauthStore, session ends on a401;createMdkQueryClient's own default when no provider is passedpasswordAuth({ baseUrl }): user name and password against an MDK stack's Gateway, through the Kernel's token issuer; the refresh token stays in an httpOnly cookie and tabs take turns refreshing it. The provider for an MDK stack, see sessionsgatewayRedirectAuth({ oauthBaseUrl })— OAuth-redirect token capture, refresh polling, and role parsing against the Gateway;MdkProvider's own default, minusoauthBaseUrl, sosignIn()throws until one is supplied
[!IMPORTANT]
gatewayRedirectAuth()expects an OAuth flow MDK does not serve: itsrefresh()posts to/auth/token, which on an MDK Gateway is service-account sign-in (a signed assertion), and/auth/userinfodoes not exist. Against an MDK stack usepasswordAuth(). KeepgatewayRedirectAuth()for a backend of your own that serves those routes.
Hiding a write control in the UI is not authorization: the Gateway still denies a request the token is not allowed to
make, checking every app route against spec.rbac. Gate controls for UX; the policy is enforced at the Gateway and the Kernel.
Reading your own data
The adapter ships no data hooks for specific endpoints. A page reads the routes
your app serves with
useQuery, shapes the response in select, and passes the result to a devkit
component as props:
import { useMdkContext, useQuery } from '@tetherto/mdk-react-adapter'
import { LineChartCard } from '@tetherto/mdk-react-devkit'
const HashrateCard = () => {
const { apiBaseUrl } = useMdkContext()
const { data, isLoading } = useQuery({
queryKey: ['site', 'history', 'hashrate'],
queryFn: () => fetch(`${apiBaseUrl}/site/history?metric=hashrate`).then((r) => r.json()),
select: toChartCardData, // your mapping from the route's shape to ChartCardData
})
return <LineChartCard title="Hashrate" data={data} isLoading={isLoading} />
}The mvp-site example does this end to end against its site plugin.
Loading, error and ready
toQueryView(query, url, { unreachableHint }) turns a query into the one value a component switches on:
{ status: 'loading' }, { status: 'error', message } or { status: 'ready', data }. The message is
describeRequestError's one line (nothing serves the route, the
Gateway refused it, nothing answered, or the app's own ERR_ message). Placeholder data counts as ready, so
with placeholderData: (previous) => previous the last body stays on screen while the next one loads.
const useStatus = (): QueryView<Status> => {
const { apiUrl } = useMdkApp()
const url = apiUrl('/status')
const query = useQuery({ queryKey: ['status', url], queryFn: () => fetchStatus(url) })
return toQueryView(query, url, { unreachableHint: 'Start the stack with `mdk run`.' })
}A report's period
usePeriod(useSearchParams(), timezone, { preset: '30d' }) keeps a reporting period in the URL
(?range=&from=&to=&granularity=), so a report can be linked to and survives a reload. It takes the
[params, setParams] pair react-router's useSearchParams() returns, so the adapter depends on no router.
Ask your route for period.query ({ start, end, granularity }) and render the devkit's PeriodControl
with the state. Rolling presets end now; re-render each minute to keep one rolling.
Machine-readable hook manifest
Every hook is listed in dist/hooks.json (regenerated at build time). Agents
and tooling load it via the subpath export:
import { hooks } from '@tetherto/mdk-react-adapter/hooks.json' with { type: 'json' }
hooks.map((h) => h.name)
hooks.filter((h) => h.category === 'permission')Subpath exports
| Subpath | Purpose |
| ------------- | --------------------------------------------- |
| . | Top-level barrel |
| ./provider | <MdkProvider> + the MdkContext it powers |
| ./hooks | Store hooks, utility hooks, TanStack re-exports |
| ./hooks.json | Machine-readable hook manifest (generated at build time) |
Build strategy
tsc -p tsconfig.build.json emits ESM JS + .d.ts declarations into
dist/, and the package exports map resolves there. External npm
consumers get pre-built declarations and runtime JS; the emitted relative
imports carry no file extension, so a bundler resolves them and TypeScript
typechecks them under "moduleResolution": "bundler".
