React Hooks

React hooks for reactive store state management, UI scaling, and aspect ratio detection

React Hooks

The TelemetryOS SDK provides React hooks for store state management and responsive UI scaling. Import from @telemetryos/sdk/react.

Store Hooks

Quick Start

// hooks/store.ts
import { createUseInstanceStoreState } from '@telemetryos/sdk/react'

// Create a typed hook for a store key
export const useTeamState = createUseInstanceStoreState<string>('team', '')
// In any component
import { useTeamState } from '../hooks/store'

function MyComponent() {
  const [isLoading, team, setTeam] = useTeamState()

  return (
    <input
      disabled={isLoading}
      value={team}
      onChange={(e) => setTeam(e.target.value)}
    />
  )
}

Scope-Specific Factory Functions

These factory functions create reusable, typed hooks for specific store keys. Each function targets a specific storage scope and manages the store reference internally.

createUseInstanceStoreState

Creates hooks for instance-scoped storage. This is the most common choice for Settings ↔ Render communication.

import { createUseInstanceStoreState } from '@telemetryos/sdk/react'

// Create typed hooks for each store key
export const useTeamState = createUseInstanceStoreState<string>('team', '')
export const useLeagueState = createUseInstanceStoreState<string>('league', 'nfl')
export const useRefreshInterval = createUseInstanceStoreState<number>('refreshInterval', 30)
// Use in components - add debounce for text inputs
const [isLoading, team, setTeam] = useTeamState(250) // 250ms debounce for text input
const [isLoading, league, setLeague] = useLeagueState() // 0ms default for dropdowns

createUseApplicationStoreState

Creates hooks for application-scoped storage. Data is shared across all instances of an application within an account.

import { createUseApplicationStoreState } from '@telemetryos/sdk/react'

// Shared across all instances
export const useGlobalConfig = createUseApplicationStoreState<Config>('globalConfig', defaultConfig)

createUseDeviceStoreState

Creates hooks for device-scoped storage. Data persists on the specific device.

import { createUseDeviceStoreState } from '@telemetryos/sdk/react'

// Device-specific cache
export const useLocalCache = createUseDeviceStoreState<CacheData>('cache', {})

Note: Device storage is only available in the Render mount point. Using it in Settings will throw an error.

createUseSharedStoreState

Creates hooks for shared storage with a namespace. Enables data sharing between different applications.

import { createUseSharedStoreState } from '@telemetryos/sdk/react'

// Inter-app communication
export const useSharedData = createUseSharedStoreState<SharedData>('data', {}, 'my-namespace')

createUseDynamicNamespaceStoreState

Creates hooks for shared storage where the namespace is determined at runtime. Unlike createUseSharedStoreState where the namespace is fixed at hook definition, the namespace is passed each time the hook is called. Use this when the namespace depends on runtime data like a user selection, route parameter, or other dynamic key.

import { createUseDynamicNamespaceStoreState } from '@telemetryos/sdk/react'

// Define with key + default only — no namespace yet
export const useQueuesState = createUseDynamicNamespaceStoreState<Queue[]>('queues', [])
export const useCountersState = createUseDynamicNamespaceStoreState<Counter[]>('counters', [])
// Pass namespace at call time — hooks re-subscribe when namespace changes
const namespace = `my-app-${selectedLocation}`

const [isLoading, queues, setQueues] = useQueuesState(namespace, 250)
const [isLoading, counters] = useCountersState(namespace)

Note: The call signature is (namespace: string, debounceDelay?: number) — namespace is the first argument. When the namespace value changes, the hook automatically unsubscribes from the old namespace and subscribes to the new one.

Return Value

All factory functions return a hook with this signature:

(debounceDelay?: number) => [isLoading: boolean, value: T, setValue: Dispatch<SetStateAction<T>>]

Exception: createUseDynamicNamespaceStoreState hooks take (namespace: string, debounceDelay?: number) since the namespace is provided at call time.

The default debounce delay is 0ms (immediate updates).

ValueDescription
isLoadingtrue until first value received from store
valueCurrent value (from store or local optimistic update)
setValueUpdates both local state and store (with optional debounce)

Debounce Guidelines

Choose debounce values based on input type for optimal user experience:

Input TypeDebounceReason
Text input250msWait for typing to pause
Textarea250msWait for typing to pause
Select/Dropdown0ms (default)Immediate feedback expected
Switch/Toggle0ms (default)Immediate feedback expected
Checkbox0ms (default)Immediate feedback expected
Radio0ms (default)Immediate feedback expected
Slider5msResponsive feel, reduced message traffic
Color picker5msResponsive feel while dragging

Store Data Patterns

Recommended: Separate Store Entry Per Field

Create individual hooks for each configuration field:

// hooks/store.ts
export const useTeamState = createUseInstanceStoreState<string>('team', '')
export const useLeagueState = createUseInstanceStoreState<string>('league', 'nfl')
export const useShowScoresState = createUseInstanceStoreState<boolean>('showScores', true)

This pattern is preferred because each field updates independently, is simpler to reason about, and has better performance (only affected components re-render).

Alternative: Rich Data Object

For tightly related data (like slideshow items), use a single object:

interface SportsSlide {
  team: string
  league: string
  displaySeconds: number
}

export const useSlidesState = createUseInstanceStoreState<SportsSlide[]>('slides', [])

Use this pattern when data is inherently a collection, fields always change together, or you need atomic updates across multiple fields.

Advanced Store Hooks

For cases requiring more control over the store slice, these lower-level APIs are available.

useStoreState

Direct hook that syncs React state with a store key. You pass the store slice explicitly.

import { useStoreState } from '@telemetryos/sdk/react'
import { store } from '@telemetryos/sdk'

function MyComponent() {
  const [isLoading, value, setValue] = useStoreState<string>(
    store().instance,  // Store slice
    'myKey',           // Key name
    'default value',   // Initial state (optional)
    300                // Debounce delay in ms (optional)
  )

  return (
    <input
      disabled={isLoading}
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  )
}
ParameterTypeDescription
storeSliceStoreSliceStore scope: store().instance, store().application, store().device, or store().shared(namespace)
keystringThe store key to sync with
initialValueTDefault value before store responds (optional)
debounceMsnumberDebounce delay for setValue in milliseconds (optional)

createUseStoreState

Factory function that creates hooks where you pass the store slice at call time.

import { createUseStoreState } from '@telemetryos/sdk/react'
import { store } from '@telemetryos/sdk'

// Create the hook
const useTeamState = createUseStoreState<string>('team', '')

// Use in component - pass store slice explicitly
const [isLoading, team, setTeam] = useTeamState(store().instance)
const [isLoading, team, setTeam] = useTeamState(store().instance, 500) // with debounce

UI Scale Hooks

TelemetryOS applications run on displays ranging from small tablets to massive video walls, often at different resolutions (Full HD, 4K, 8K). The SDK provides React hooks that solve responsive scaling for display-based applications.

The Problem with CSS Units

The default rem unit equals 16 pixels. This pixel-based definition means applications appear differently sized depending on display resolution — a 16px font on a 4K TV appears half the size relative to the screen compared to Full HD.

By redefining rem as 1% of the viewport's long dimension (1vmax), applications scale consistently across any display resolution.

:root {
  font-size: 1vmax; /* 1rem = 1% of viewport long side */
}

useUiScaleToSetRem

Sets the document's root font-size based on viewport size and scale factor. Call this once in the Render view to enable rem-based responsive design.

import { store } from '@telemetryos/sdk'
import { useUiScaleToSetRem } from '@telemetryos/sdk/react'
import { useUiScaleStoreState } from '../hooks/store'

function Render() {
  const [_isLoading, uiScale] = useUiScaleStoreState(store().instance)
  useUiScaleToSetRem(uiScale)

  return <div>Your content here</div>
}

The hook sets document.documentElement.style.fontSize to calc(1vmax * ${uiScale}):

  • At uiScale = 1: 1rem = 1% of viewport long side
  • At uiScale = 2: 1rem = 2% of viewport long side (everything doubles)
  • At uiScale = 1.5: 1rem = 1.5% of viewport long side
ParameterTypeDescription
uiScalenumberScale multiplier (typically 1-3)

useUiAspectRatio

Returns the current window aspect ratio and updates automatically on resize.

import { useUiAspectRatio } from '@telemetryos/sdk/react'

function ResponsiveLayout() {
  const aspectRatio = useUiAspectRatio()
  const isLandscape = aspectRatio > 1
  const isPortrait = aspectRatio < 1

  return (
    <div className={isLandscape ? 'horizontal-layout' : 'vertical-layout'}>
      {/* Adapt layout based on orientation */}
    </div>
  )
}
TypeDescription
numberCurrent aspect ratio (window.innerWidth / window.innerHeight). > 1 = landscape, < 1 = portrait, = 1 = square

useUiResponsiveFactors

Calculates width and height scaling factors for responsive layouts.

import { useUiAspectRatio, useUiResponsiveFactors } from '@telemetryos/sdk/react'
import { useUiScaleStoreState } from '../hooks/store'

function AdaptiveContent() {
  const [_, uiScale] = useUiScaleStoreState(store().instance)
  const aspectRatio = useUiAspectRatio()
  const { uiWidthFactor, uiHeightFactor } = useUiResponsiveFactors(uiScale, aspectRatio)

  const showSidebar = uiWidthFactor > 0.5

  return (
    <div>
      <main>Primary content</main>
      {showSidebar && <aside>Secondary content</aside>}
    </div>
  )
}
ParameterTypeDescription
uiScalenumberCurrent scale multiplier
uiAspectRationumberCurrent aspect ratio from useUiAspectRatio()
Return PropertyTypeDescription
uiWidthFactornumberApp width relative to screen long side
uiHeightFactornumberApp height relative to screen long side

Minimum Sizing Best Practice

Broadcast standards define a Title Safe Area (SMPTE ST 2046-1) as 90% of screen dimensions — approximately 3rem of padding from screen edges. Industry guidelines recommend body text be no smaller than 2rem for comfortable viewing at typical distances.

.app {
  padding: 3rem; /* Title safe zone */
  font-size: 2rem; /* Minimum readable size */
}

h1 {
  font-size: 6rem;
}

What’s Next