Hooks — MeoNode UI
@meonode/ui ships three hooks: one for theming, one for managing the portal stack, and one for high-frequency data updates that should bypass React's render cycle.
useTheme
Reads the current theme from context and lets you swap it.
const { theme, setTheme } = useTheme()
| Field | Description |
|---|---|
theme | The current Theme object — narrowed by your MeoTheme augmentation if present. |
setTheme | Replaces the theme. Accepts a new Theme object or an updater function (prev) => Theme. |
import { useTheme, Button } from '@meonode/ui' import { lightTheme, darkTheme } from './theme' const ThemeToggle = () => { const { theme, setTheme } = useTheme() return Button(theme.mode === 'light' ? 'Dark' : 'Light', { onClick: () => setTheme(prev => (prev.mode === 'light' ? darkTheme : lightTheme)), }) }
Side effects. When theme.mode changes, useTheme automatically:
- writes the mode to
localStorageunder the keytheme - sets
data-theme="<mode>"on<html> - toggles
light-theme/dark-themeclasses on<html>
Use those hooks (the attribute or class) to scope global CSS without coupling to JS state.
Throws if used outside a ThemeProvider.
usePortal
Imperative interface for opening, updating, and closing portal layers.
const portal = usePortal<T>(autoSyncData?)
| Argument | Description |
|---|---|
autoSyncData (optional) | A value (state, props, anything) that gets pushed to the most recently opened portal whenever it changes. Lets the portal stay in sync with the parent without manual updateData calls. |
| Method | Description |
|---|---|
portal.open(Component, initialData?) | Opens a new portal layer. If initialData is omitted, falls back to autoSyncData. Returns a PortalHandle. |
portal.close() | Closes the most recently opened layer from this hook instance. |
portal.updateData(next) | Manually pushes new data to the most recently opened layer. |
portal.handle | Ref holding the current PortalHandle, or null when no layer from this hook instance is open. |
import { useState } from 'react' import { usePortal, Button } from '@meonode/ui' import { CounterModal } from './CounterModal' const Counter = () => { const [count, setCount] = useState(0) // CounterModal will receive { count, setCount } and re-sync on every change. const portal = usePortal({ count, setCount }) return Button('Open Counter', { onClick: () => portal.open(CounterModal) }) }
Throws if used outside a PortalProvider. See the Portal System guide for the full setup and the PortalLayerProps shape.
useDataChannel
Subscribes to a DataChannel created by createDataChannel. Updates the consumer without re-rendering its parent — useful for high-frequency values like progress, drag offsets, or live counters.
const value = useDataChannel(channel)
| Argument | Description |
|---|---|
channel | A DataChannel<T> instance (or null / undefined). |
Returns the current channel value, or undefined if no channel is provided.
import { Component, createDataChannel, useDataChannel, Div } from '@meonode/ui' const progressChannel = createDataChannel(0) // Wrap the consumer in `Component` so the subscription lives in its own scope const ProgressBar = Component(function ProgressBar() { const progress = useDataChannel(progressChannel) ?? 0 return Div({ width: `${progress}%`, height: 4, backgroundColor: 'theme.primary', }) }) // Update from anywhere — including outside the React tree progressChannel.set(50)
The consumer must be its own component. useDataChannel holds state in whichever component calls it, so a plain
function called inline (Column({ children: [ProgressBar()] })) subscribes in the parent's scope and re-renders the
parent on every push — exactly what the channel exists to avoid. Wrapping in Component (or reaching it through
Node()) keeps the update confined to the consumer.
channel.set(next) takes a value, not an updater function. Use channel.get() to read the current value if you need to
compute the next one, and channel.subscribe(cb) to observe changes outside React — it returns an unsubscribe function.
Related FAQ
Why do I get a "Rendered fewer hooks than expected" error with conditional components?
This happens when hook execution order changes between renders. Keep hook-using components behind stable boundaries (Node() or Component) and avoid calling them conditionally as plain functions.
How do node functions work?
Node functions are composable factories. Use children-first signatures for text/content nodes and props-first signatures for layout/container nodes.
More details: /docs/getting-started/faq
On this page
- Hooks — MeoNode UI