• 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.
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 { 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.


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