MeoNode UI
  • Getting Started
    • Overview
    • Why Without JSX?
    • Installation
    • Usage
    • Styling
    • Theming
    • Portal System
    • Rules & Patterns
    • Framework Integration
    • FAQ
    • Release Notes
  • MUI Integration
  • Components
  • Hooks

Portal System

The MeoNode UI Portal System provides a robust, stack-based solution for managing overlays like modals, drawers, and tooltips. It supports nested portals, state synchronization, and imperative control with full type safety.

Architecture

The system consists of three main parts:

  1. PortalProvider: Manages the portal stack and provides the context.
  2. PortalHost: The target container where portals are rendered (usually at the root).
  3. usePortal Hook: The primary interface for opening and closing portals.

Basic Setup

To use the portal system, you must wrap your application (or the relevant branch) with PortalProvider and include a PortalHost where you want the portals to be injected.

import { PortalProvider, PortalHost, Div } from '@meonode/ui'

const Layout = ({ children }) => 
  PortalProvider({
    children: [
      children,
      PortalHost() // Usually placed at the end to render on top
    ]
  })

Using usePortal Hook

The usePortal hook is the primary way to interact with the portal system.

import { usePortal, Button, Div, Text } from '@meonode/ui'

// 1. Define your portal content component
const MyModal = ({ data, close }) => 
  Div({
    padding: 24,
    backgroundColor: 'theme.base',
    children: [
      Text(\`Hello, \${data.name}!\`),
      Button('Close', { onClick: close })
    ]
  })

// 2. Use the hook in your component
const App = () => {
  const portal = usePortal()

  return Button('Open Modal', {
    onClick: () => portal.open(MyModal, { name: 'World' })
  })
}

Hook API

  • portal.open(Component, data?): Opens a new portal layer.
  • portal.close(): Closes the current (top-most) portal layer managed by this hook instance.
  • portal.updateData(data): Updates the data of the current portal layer without re-rendering the parent.

Auto-Sync Feature

One of the most powerful features of MeoNode's portal system is automatic data synchronization. When you pass a data object to the usePortal hook, any portal opened by that hook instance will automatically stay in sync with that data.

This means when the parent component re-renders with new state, the portal content receives the updated data automatically without having to manually call portal.updateData().

import React, { useState } from 'react'
import { usePortal, Button, Div, Text, type PortalLayerProps } from '@meonode/ui'

// 1. Define the type of the data object.
interface CountData {
  count: number
  setCount: React.Dispatch<React.SetStateAction<number>>
}

// 2. Define the portal content. 
// It receives the synced data via the 'data' prop.
const CounterModal = ({ data, close }: PortalLayerProps<CounterData>) => 
  Div({
    padding: 24,
    backgroundColor: 'theme.base',
    children: [
      Text(`Current count: ${data.count}`),
      Button('Increment Count', {
        onClick: () => data.setCount(c => c + 1)
      }),
      Button('Close', { onClick: close })
    ]
  })

const MyComponent = () => {
  const [count, setCount] = useState(0)
  
  // 3. Pass the state (or any object) to usePortal.
  // This enables auto-sync for all portals opened by this hook instance.
  const portal = usePortal({ count, setCount })

  return Div({
    children: [
      Button('Open Counter Modal', { 
        onClick: () => portal.open(CounterModal) 
      }),
    ]
  })
}

Data Channels

For high-frequency updates or state that shouldn't trigger parent re-renders, you can use createDataChannel.

import { createDataChannel, useDataChannel } from '@meonode/ui'

const countChannel = createDataChannel(0)

const PortalContent = () => {
  const count = useDataChannel(countChannel)
  return Text(\`Count: \${count}\`)
}

// Update from anywhere
countChannel.set(prev => prev + 1)

Nested Portals

Portals can open other portals. The system automatically manages the stack and depth.

const Outer = () => {
  const portal = usePortal()
  return Button('Open Inner', {
    onClick: () => portal.open(Inner)
  })
}

On this page