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. |
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 { createDataChannel, useDataChannel, Div } from '@meonode/ui' const progressChannel = createDataChannel(0) const ProgressBar = () => Div({ width: `${useDataChannel(progressChannel) ?? 0}%`, height: 4, backgroundColor: 'theme.primary', }) // Update from anywhere — including outside the React tree progressChannel.set(50)
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.
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