• MUI Integration
  • Components
  • Hooks

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()
FieldDescription
themeThe current Theme object — narrowed by your MeoTheme augmentation if present.
setThemeReplaces 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 localStorage under the key theme
  • sets data-theme="<mode>" on <html>
  • toggles light-theme / dark-theme classes 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?)
ArgumentDescription
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.
MethodDescription
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.handleRef 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)
ArgumentDescription
channelA 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.


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