Component hooks
Hooks coupled to MDK styled components or shell layout
@tetherto/mdk-react-devkit
Hooks that wrap or compose styled MDK components — notifications, sidebar/header shell, forms, filters, widgets, tables, charts, and dashboards.
Adopt these when you are using @tetherto/mdk-react-devkit for your UI.
If you bring your own components, you may not need anything here. Start with State hooks or Utility hooks instead.
At a glance
| Sub-group | Hooks |
|---|---|
| Notifications | useNotification |
| Shell | useHeaderControls, useSidebarExpandedState, useSidebarSectionState |
| Forms | useFormField, useFormReset |
| Filters | useReportTimeFrameSelectorState, useTimeframeControls |
| Widgets | useFinancialDateRange |
| Tables | useGetAvailableDevices |
| Charts | useChartDataCheck, useEbitda, useEnergyBalanceViewModel |
| Dashboards | usePoolConfigs, useSiteOverviewDetailsData |
| Reporting | useHashrate, useEnergyReportSite |
Prerequisites
- Complete the @tetherto/mdk-react-devkit installation and add the dependency
- For hooks that read from headless stores (
useNotification, view-model hooks): wrap your app in<MdkProvider>
Import
import {
useChartDataCheck,
useEbitda,
useEnergyBalanceViewModel,
useFinancialDateRange,
useGetAvailableDevices,
useHeaderControls,
useNotification,
usePoolConfigs,
useReportTimeFrameSelectorState,
useSiteOverviewDetailsData,
useTimeframeControls,
} from '@tetherto/mdk-react-devkit/foundation'
import {
useFormField,
useFormReset,
useSidebarExpandedState,
useSidebarSectionState,
} from '@tetherto/mdk-react-devkit/core'Notifications
useNotification
@tetherto/mdk-react-devkit/foundation
Show toast notifications backed by the headless notificationStore. Supports success, error, info, and warning variants.
import { useNotification } from '@tetherto/mdk-react-devkit/foundation'Returns
| Member | Type | Description |
|---|---|---|
notifySuccess | function | Show success toast |
notifyError | function | Show error toast |
notifyInfo | function | Show info toast |
notifyWarning | function | Show warning toast |
Method signature
notifySuccess(message: string, description?: string, options?: NotificationOptions)Options
Notification methods accept an optional third options argument:
| Option | Status | Type | Default | Description |
|---|---|---|---|---|
duration | Optional | number | 3000 | Duration in milliseconds (0 = no autoclose) |
position | Optional | ToastPosition | 'top-left' | Toast position on screen |
dontClose | Optional | boolean | false | When true, prevents autoclose |
Example
function SaveButton() {
const { notifySuccess, notifyError } = useNotification()
const handleSave = async () => {
try {
await saveData()
notifySuccess('Saved', 'Your changes have been saved.')
} catch (error) {
notifyError('Error', 'Failed to save changes.', { dontClose: true })
}
}
return <Button onClick={handleSave}>Save</Button>
}Shell
useHeaderControls
@tetherto/mdk-react-devkit/foundation
Read/write hook for the global header-controls store (toggles, sticky flag, theme).
import { useHeaderControls } from '@tetherto/mdk-react-devkit/foundation'Returns
| Member | Type | Description |
|---|---|---|
preferences | HeaderPreferences | Current visibility state for each header item |
isLoading | boolean | Loading state |
error | Error | null | Error state |
handleToggle | function | Toggle a header item visibility |
handleReset | function | Reset to default preferences |
handleToggle and handleReset both call notifySuccess internally. Every invocation produces a toast notification; avoid calling them in response to fast-changing state.
Example
function HeaderSettings() {
const { preferences, handleToggle, handleReset } = useHeaderControls()
return (
<div>
{Object.entries(preferences).map(([key, visible]) => (
<Toggle
key={key}
label={key}
checked={visible}
onChange={(value) => handleToggle(key, value)}
/>
))}
<Button onClick={handleReset}>Reset to Default</Button>
</div>
)
}useSidebarExpandedState
@tetherto/mdk-react-devkit/core
Persist sidebar expanded/collapsed state in localStorage so the layout survives reloads.
import { useSidebarExpandedState } from '@tetherto/mdk-react-devkit/core'Example
function AppSidebar() {
const [expanded, setExpanded] = useSidebarExpandedState(false)
return (
<aside className={expanded ? 'sidebar--expanded' : 'sidebar--collapsed'}>
<Button onClick={() => setExpanded(!expanded)}>Toggle sidebar</Button>
</aside>
)
}useSidebarSectionState
@tetherto/mdk-react-devkit/core
Persist individual sidebar section open/closed states in localStorage.
import { useSidebarSectionState } from '@tetherto/mdk-react-devkit/core'Example
function SidebarSection({ id, title, children }) {
const [open, setOpen] = useSidebarSectionState(id, true)
return (
<section>
<button type="button" onClick={() => setOpen(!open)}>{title}</button>
{open ? children : null}
</section>
)
}Forms
useFormField
@tetherto/mdk-react-devkit/core
Read-only context hook for form field children — returns the field's id, error state, and ARIA attributes.
import { useFormField } from '@tetherto/mdk-react-devkit/core'Example
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage, useFormField } from '@tetherto/mdk-react-devkit/core'
// Custom component that reads field context — must be rendered inside <FormField> / <FormItem>
function FieldStatusDot() {
const { invalid, isDirty } = useFormField()
return (
<span className={invalid ? 'dot--error' : isDirty ? 'dot--dirty' : 'dot--clean'} />
)
}useFormReset
@tetherto/mdk-react-devkit/core
Hook to handle form reset with callbacks.
import { useFormReset } from '@tetherto/mdk-react-devkit/core'Example
import { useForm } from 'react-hook-form'
import { Form, FormInput, useFormReset } from '@tetherto/mdk-react-devkit/core'
import { Button } from '@tetherto/mdk-react-devkit/core'
type MinerFields = { name: string; ip: string }
function MinerEditForm({ onSubmit }: { onSubmit: (v: MinerFields) => void }) {
const form = useForm<MinerFields>({ defaultValues: { name: '', ip: '' } })
const { resetForm, isDirty } = useFormReset({
form,
onAfterReset: () => console.log('Form reset'),
})
return (
<Form form={form} onSubmit={form.handleSubmit(onSubmit)}>
<FormInput control={form.control} name="name" label="Name" />
<FormInput control={form.control} name="ip" label="IP address" />
<Button type="submit">Save</Button>
<Button type="button" onClick={resetForm} disabled={!isDirty}>
Reset
</Button>
</Form>
)
}Filters
useReportTimeFrameSelectorState
@tetherto/mdk-react-devkit/foundation
State hook backing the reporting time-frame selector — exposes the active window and setters.
import { useReportTimeFrameSelectorState } from '@tetherto/mdk-react-devkit/foundation'Example
import { useReportTimeFrameSelectorState } from '@tetherto/mdk-react-devkit/foundation'
function ReportDateBar() {
const { start, end, presetTimeFrame, setPresetTimeFrame } = useReportTimeFrameSelectorState()
return (
<div>
<p>{start.toLocaleDateString()} – {end.toLocaleDateString()}</p>
<button onClick={() => setPresetTimeFrame(7)} className={presetTimeFrame === 7 ? 'active' : ''}>Last 7 days</button>
<button onClick={() => setPresetTimeFrame(30)} className={presetTimeFrame === 30 ? 'active' : ''}>Last 30 days</button>
<button onClick={() => setPresetTimeFrame(null)}>Custom range</button>
</div>
)
}useTimeframeControls
@tetherto/mdk-react-devkit/foundation
Core state machine for TimeframeControls — owns year / month / week selection and resolves the date-range output.
import { useTimeframeControls } from '@tetherto/mdk-react-devkit/foundation'Example
import { useTimeframeControls } from '@tetherto/mdk-react-devkit/foundation'
function YearMonthPicker({ dateRange, onRangeChange, onTimeframeTypeChange }) {
const {
selectedYear,
selectedMonth,
handleYearChange,
handleMonthTreeChange,
yearSelectValue,
monthSelectValue,
} = useTimeframeControls({
dateRange,
timeframeType: null,
onRangeChange,
onTimeframeTypeChange,
isWeekSelectVisible: false,
weekTree: false,
})
return (
<div>
<select value={yearSelectValue} onChange={(e) => handleYearChange(e.target.value)}>
<option value={String(selectedYear)}>{selectedYear}</option>
</select>
<select value={monthSelectValue} onChange={(e) => handleMonthTreeChange(e.target.value)}>
<option value={monthSelectValue}>Month {selectedMonth + 1}</option>
</select>
</div>
)
}Widgets
useFinancialDateRange
@tetherto/mdk-react-devkit/foundation
Resolves the active financial date range (start/end) used by every reporting-section query.
import { useFinancialDateRange } from '@tetherto/mdk-react-devkit/foundation'Example
import { useFinancialDateRange } from '@tetherto/mdk-react-devkit/foundation'
import { Button } from '@tetherto/mdk-react-devkit/core'
function ReportingToolbar({ timezone }: { timezone: string }) {
const { datePicker, dateRange, onDateRangeReset } = useFinancialDateRange({ timezone })
return (
<div>
{datePicker}
<Button onClick={onDateRangeReset}>Reset to current month</Button>
{dateRange && (
<p>
{new Date(dateRange.start).toLocaleDateString()} –{' '}
{new Date(dateRange.end).toLocaleDateString()}
</p>
)}
</div>
)
}Tables
useGetAvailableDevices
@tetherto/mdk-react-devkit/foundation
Transforms the host's device list into the available container and miner type sets used by device explorer. Pass data from your query result.
import { useGetAvailableDevices } from '@tetherto/mdk-react-devkit/foundation'Example
import { useGetAvailableDevices } from '@tetherto/mdk-react-devkit/foundation'
function DeviceTypeFilter({ devices }) {
const { availableContainerTypes, availableMinerTypes } = useGetAvailableDevices({ data: devices })
return (
<div>
<select aria-label="Container type">
<option value="">All containers</option>
{availableContainerTypes.map((type) => <option key={type}>{type}</option>)}
</select>
<select aria-label="Miner type">
<option value="">All miners</option>
{availableMinerTypes.map((type) => <option key={type}>{type}</option>)}
</select>
</div>
)
}Charts
useChartDataCheck
@tetherto/mdk-react-devkit/foundation
Check if chart data is empty or unavailable. Returns true if empty (show empty state), false if data exists (show chart).
import { useChartDataCheck } from '@tetherto/mdk-react-devkit/foundation'Pass chart input in one of two shapes:
dataset: direct dataset for BarChart-style usage.data: Chart.js-shaped object withdatasets(LineChart) or adatasetproperty.
Provide at least one of dataset or data for a meaningful empty check.
Options
| Option | Status | Type | Default | Description |
|---|---|---|---|---|
dataset | Optional | object | array | none | Direct dataset for BarChart; set dataset or data (at least one) for a meaningful check |
data | Optional | object | none | Chart.js-shaped object with datasets (LineChart) or dataset property; set dataset or data (at least one) |
Returns
| Type | Description |
|---|---|
boolean | true if data is empty, false if data exists |
Example
function HashrateChart({ dataset }) {
const isEmpty = useChartDataCheck({ dataset })
if (isEmpty) {
return <EmptyState message="No hashrate data available" />
}
return <BarChart data={dataset} />
}function TemperatureChart({ data }) {
const isEmpty = useChartDataCheck({ data })
return isEmpty ? (
<EmptyState message="No temperature data" />
) : (
<LineChart data={data} />
)
}Chart utility integration
useChartDataCheck expects Chart.js-shaped data ({ labels, datasets }), not raw { labels, series } from app hooks. Convert hook output with
the buildBarChartData utility from @tetherto/mdk-react-devkit/core,
then pass the result to useChartDataCheck.
import { BarChart, buildBarChartData, ChartContainer } from '@tetherto/mdk-react-devkit/core'
import { useChartDataCheck } from '@tetherto/mdk-react-devkit/foundation'
function RevenueBarChart({ hookOutput }) {
const chartData = buildBarChartData(hookOutput)
const isEmpty = useChartDataCheck({ data: chartData })
return (
<ChartContainer title="Revenue" empty={isEmpty}>
<BarChart data={chartData} />
</ChartContainer>
)
}For the full BarChartInput shape, per-dataset datalabels merge, and all-zero empty rules, see
Hook-shaped bar data (buildBarChartData).
useEbitda
@tetherto/mdk-react-devkit/foundation
Transforms an EbitdaResponse and date-range options into query params and a chart-ready EBITDA view-model.
import { useEbitda } from '@tetherto/mdk-react-devkit/foundation'Example
import { useEbitda } from '@tetherto/mdk-react-devkit/foundation'
// Wire your query result in; consume queryParams to drive the fetch
function EbitdaSection({ ebitdaResponse, isLoading, fetchErrors }) {
const { datePicker, dateRange, queryParams, errors } = useEbitda({
ebitda: ebitdaResponse,
isLoading,
fetchErrors,
})
// Pass queryParams to your data-fetching layer whenever the date range changes
// e.g. useGetEbitdaQuery(queryParams, { skip: !queryParams })
return (
<div>
{datePicker}
{errors.length > 0 && <p role="alert">{errors.join(', ')}</p>}
</div>
)
}useEnergyBalanceViewModel
@tetherto/mdk-react-devkit/foundation
Computes the full EnergyBalance view model from raw API data, managing tab selection and display-mode state.
import { useEnergyBalanceViewModel } from '@tetherto/mdk-react-devkit/foundation'Example
import { useEnergyBalanceViewModel } from '@tetherto/mdk-react-devkit/foundation'
import { Button } from '@tetherto/mdk-react-devkit/core'
function EnergyBalancePanel({ data, isLoading, fetchErrors, dateRange, availablePowerMW }) {
const { viewModel, onTabChange, onRevenueDisplayModeChange } = useEnergyBalanceViewModel({
data,
isLoading,
fetchErrors,
dateRange,
availablePowerMW,
})
return (
<div>
<div>
<Button onClick={() => onTabChange('revenue')} disabled={viewModel.activeTab === 'revenue'}>Revenue</Button>
<Button onClick={() => onTabChange('cost')} disabled={viewModel.activeTab === 'cost'}>Cost</Button>
</div>
{viewModel.isLoading && <p>Loading…</p>}
{viewModel.errors.length > 0 && <p role="alert">{viewModel.errors.join(', ')}</p>}
</div>
)
}Dashboards
usePoolConfigs
@tetherto/mdk-react-devkit/foundation
Transforms raw pool-configuration rows from your API into PoolSummary objects for the Pool Manager UI. Fetch with TanStack Query in the host app, then pass data, isLoading, and error into this hook.
Typical usage: fetch with TanStack Query in the host app, then pass data, isLoading, and error into this hook. Foundation components such as PoolManagerPools and Miner explorer expect data shaped this way.
import { usePoolConfigs } from '@tetherto/mdk-react-devkit/foundation'Options
| Option | Status | Type | Default | Description |
|---|---|---|---|---|
data | Optional | PoolConfigData[] | none | Raw pool configuration rows from your API |
isLoading | Optional | boolean | false | When true, the host should show a loading state |
error | Optional | unknown | none | Error from your query; surfaced to pool-manager components |
Returns
| Member | Type | Description |
|---|---|---|
pools | PoolSummary[] | Normalized pool list for lists and accordions |
poolIdMap | Record<string, PoolSummary> | Lookup by pool id |
isLoading | boolean | Same as the option you passed in |
error | unknown | Same as the option you passed in |
Example
import { useGetPoolConfigsQuery } from '@/app/services/api'
import { usePoolConfigs } from '@tetherto/mdk-react-devkit/foundation'
export function useAppPoolConfigs() {
const { data, isLoading, error } = useGetPoolConfigsQuery({})
return usePoolConfigs({ data, isLoading, error })
}function PoolsPage({ poolConfig }: { poolConfig: PoolConfigData[] }) {
const { pools, isLoading, error } = usePoolConfigs({ data: poolConfig })
if (isLoading) return <Loader />
if (error) return <CoreAlert variant="error">Failed to load pools</CoreAlert>
return (
<ul>
{pools.map((pool) => (
<li key={pool.id}>{pool.name}</li>
))}
</ul>
)
}useSiteOverviewDetailsData
@tetherto/mdk-react-devkit/foundation
Composes the per-site overview view-model: pools, performance series, and recent activity.
import { useSiteOverviewDetailsData } from '@tetherto/mdk-react-devkit/foundation'Example
import { useSiteOverviewDetailsData } from '@tetherto/mdk-react-devkit/foundation'
function SiteOverviewCard({ unit, pdus, connectedMiners, isLoading }) {
const {
containerHashRate,
isContainerRunning,
minersHashmap,
segregatedPduSections,
} = useSiteOverviewDetailsData(unit, { pdus, connectedMiners, isLoading })
return (
<div>
<p>Hashrate: {containerHashRate}</p>
<p>Status: {isContainerRunning ? 'Running' : 'Offline'}</p>
<p>Miners mapped: {Object.keys(minersHashmap).length}</p>
<p>PDU sections: {Object.keys(segregatedPduSections).join(', ')}</p>
</div>
)
}Reporting
useHashrate
@tetherto/mdk-react-devkit/foundation
Base hook for a single Hashrate tab in single-site mode. Normalises a grouped-hashrate query result into the { log, isLoading, error } shape consumed by <HashrateSiteView>, <HashrateMinerTypeView>, and <HashrateMiningUnitView>. Call once per tab — each tab fetches independently because they use different groupBy axes.
import { useHashrate } from '@tetherto/mdk-react-devkit/foundation'Options
| Option | Status | Type | Default | Description |
|---|---|---|---|---|
query | Optional | HashrateQueryState | none | Result of fetching the v2 /auth/metrics/hashrate?groupBy=… endpoint. Wire your data layer (TanStack Query, RTK Query, fixtures) and pass the result here — this hook never fetches directly. |
Returns
| Member | Type | Description |
|---|---|---|
log | HashrateGroupedLog | undefined | Normalised grouped-hashrate log; undefined while loading. |
isLoading | boolean | Loading state from the upstream query. |
error | unknown | Error from the upstream query, if any. |
Example
import { useQuery } from '@tanstack/react-query'
import { useHashrate } from '@tetherto/mdk-react-devkit/foundation'
import { HashrateSiteView } from '@tetherto/mdk-react-devkit/foundation'
function HashrateTab({ groupBy }) {
const query = useQuery({ queryKey: ['hashrate', groupBy], queryFn: fetchHashrate })
const { log, isLoading, error } = useHashrate({ query })
return <HashrateSiteView log={log} isLoading={isLoading} error={error} />
}useEnergyReportSite
@tetherto/mdk-react-devkit/foundation
Merges site energy consumption data (from the v2 /auth/metrics/consumption endpoint) with snapshot tail-log and container list data for the Energy report site tab. Returns the combined view-model consumed by the energy report components.
import { useEnergyReportSite } from '@tetherto/mdk-react-devkit/foundation'Options
| Option | Status | Type | Default | Description |
|---|---|---|---|---|
dateRange | Required | object | none | Required. Active date range { start, end } in ms epoch. |
consumptionLog | Required | object | none | Required. Raw consumption log from the /auth/metrics/consumption response. |
consumptionLoading | Required | boolean | none | Required. Loading state of the consumption query. |
consumptionFetching | Required | boolean | none | Required. Background-refetch state of the consumption query. |
consumptionError | Required | unknown | none | Required. Error state of the consumption query. |
nominalPowerAvailabilityMw | Required | number | undefined | none | Required. Site nominal power capacity in MW from the nominal config. |
nominalConfigLoading | Required | boolean | none | Required. Loading state of the nominal config query. |
tailLog | Required | object | none | Required. Raw tail-log snapshot data. |
tailLogLoading | Required | boolean | none | Required. Loading state of the tail-log query. |
containers | Required | object[] | none | Required. Container list from the device query. |
containersLoading | Required | boolean | none | Required. Loading state of the container query. |
Returns
Returns a UseEnergyReportSiteResult object containing the merged power-consumption view-model, power-mode table rows, and combined loading/error states for each data source. Pass directly to the Energy report site-tab components.