MDK Logo

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-groupHooks
NotificationsuseNotification
ShelluseHeaderControls, useSidebarExpandedState, useSidebarSectionState
FormsuseFormField, useFormReset
FiltersuseReportTimeFrameSelectorState, useTimeframeControls
WidgetsuseContainerThresholds, useFinancialDateRange
TablesuseGetAvailableDevices
ChartsuseChartDataCheck, useEbitda, useEnergyBalanceViewModel
DashboardsusePoolConfigs, useSiteOverviewDetailsData

Prerequisites

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

MemberTypeDescription
notifySuccessfunctionShow success toast
notifyErrorfunctionShow error toast
notifyInfofunctionShow info toast
notifyWarningfunctionShow warning toast

Method signature

notifySuccess(message: string, description?: string, options?: NotificationOptions)

Options

Notification methods accept an optional third options argument:

OptionStatusTypeDefaultDescription
durationOptionalnumber3000Duration in milliseconds (0 = no autoclose)
positionOptionalToastPosition'top-left'Toast position on screen
dontCloseOptionalbooleanfalseWhen 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

MemberTypeDescription
preferencesHeaderPreferencesCurrent visibility state for each header item
isLoadingbooleanLoading state
errorError | nullError state
handleTogglefunctionToggle a header item visibility
handleResetfunctionReset 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:

  1. dataset: direct dataset for BarChart-style usage.
  2. data: Chart.js-shaped object with datasets (LineChart) or a dataset property.

Provide at least one of dataset or data for a meaningful empty check.

Options

OptionStatusTypeDefaultDescription
datasetOptionalobject | arraynoneDirect dataset for BarChart; set dataset or data (at least one) for a meaningful check
dataOptionalobjectnoneChart.js-shaped object with datasets (LineChart) or dataset property; set dataset or data (at least one)

Returns

TypeDescription
booleantrue 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

OptionStatusTypeDefaultDescription
dataOptionalPoolConfigData[]noneRaw pool configuration rows from your API
isLoadingOptionalbooleanfalseWhen true, the host should show a loading state
errorOptionalunknownnoneError from your query; surfaced to pool-manager components

Returns

MemberTypeDescription
poolsPoolSummary[]Normalized pool list for lists and accordions
poolIdMapRecord<string, PoolSummary>Lookup by pool id
isLoadingbooleanSame as the option you passed in
errorunknownSame 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>
  )
}

On this page