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 dropdownscreateUseApplicationStoreState
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).
| Value | Description |
|---|---|
isLoading | true until first value received from store |
value | Current value (from store or local optimistic update) |
setValue | Updates both local state and store (with optional debounce) |
Debounce Guidelines
Choose debounce values based on input type for optimal user experience:
| Input Type | Debounce | Reason |
|---|---|---|
| Text input | 250ms | Wait for typing to pause |
| Textarea | 250ms | Wait for typing to pause |
| Select/Dropdown | 0ms (default) | Immediate feedback expected |
| Switch/Toggle | 0ms (default) | Immediate feedback expected |
| Checkbox | 0ms (default) | Immediate feedback expected |
| Radio | 0ms (default) | Immediate feedback expected |
| Slider | 5ms | Responsive feel, reduced message traffic |
| Color picker | 5ms | Responsive 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)}
/>
)
}| Parameter | Type | Description |
|---|---|---|
storeSlice | StoreSlice | Store scope: store().instance, store().application, store().device, or store().shared(namespace) |
key | string | The store key to sync with |
initialValue | T | Default value before store responds (optional) |
debounceMs | number | Debounce 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 debounceUI 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
| Parameter | Type | Description |
|---|---|---|
uiScale | number | Scale 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>
)
}| Type | Description |
|---|---|
number | Current 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>
)
}| Parameter | Type | Description |
|---|---|---|
uiScale | number | Current scale multiplier |
uiAspectRatio | number | Current aspect ratio from useUiAspectRatio() |
| Return Property | Type | Description |
|---|---|---|
uiWidthFactor | number | App width relative to screen long side |
uiHeightFactor | number | App 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;
}Updated about 1 hour ago