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 | useContainerThresholds, useFinancialDateRange |
| Tables | useGetAvailableDevices |
| Charts | useChartDataCheck, useEbitda, useEnergyBalanceViewModel |
| Dashboards | usePoolConfigs, useSiteOverviewDetailsData |
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,
useContainerThresholds,
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 |
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
useContainerThresholds
@tetherto/mdk-react-devkit/foundation
Hook that reads and updates the temperature/pressure/power thresholds for a single container.
import { useContainerThresholds } from '@tetherto/mdk-react-devkit/foundation'Example
import { useContainerThresholds } from '@tetherto/mdk-react-devkit/foundation'
import { Button, Input } from '@tetherto/mdk-react-devkit/core'
function ContainerThresholdsEditor({ containerData }) {
const {
thresholds,
isEditing,
isSaving,
handleThresholdChange,
handleSave,
handleReset,
} = useContainerThresholds({ data: containerData })
return (
<div>
<Input
value={(thresholds as any)?.temperature?.alarm ?? ''}
onChange={(e) => handleThresholdChange('temperature', 'alarm', e.target.value)}
label="Temperature alarm (°C)"
/>
<Button onClick={handleSave} disabled={isSaving || !isEditing}>Save</Button>
<Button onClick={handleReset} disabled={isSaving}>Reset to defaults</Button>
</div>
)
}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>
)
}