MeoNode UI
  • Getting Started
    • Overview
    • Installation
    • Usage
    • Styling
    • Theming
    • Rules & Patterns
    • Framework Integration
    • FAQ
    • Release Notes
  • Components

Usage — MeoNode UI

Build type-safe React UIs using function composition instead of JSX. This guide covers core patterns, component architecture, and integration strategies for @meonode/ui.


Node Fundamentals

MeoNode uses node functions (Column, H1, Button, Text, etc.) as building blocks. Each represents an HTML element or component with type-safe props.

Text-first (children-first) Nodes

Nodes intended for textual content accept the text or children as the first argument (string, number, array of nodes, etc.) and an optional props object as the second — keeping simple usage concise while still allowing full prop-based styling when needed.

Applies to: H1H6, Text, Span, Button, A, Label, Code, etc

import { Column, H1, Text, Span, Button } from '@meonode/ui'

const WelcomeSection = () =>
  Column({
    padding: 20, // Numbers → pixels (20px)
    children: [
      H1('Welcome to our App!', {
        fontSize: '2.5rem',
        color: '#333',
        marginBottom: 15,
      }),
      Text('Build UIs with function composition.', {
        fontSize: '1rem',
        color: '#666',
        lineHeight: '1.5',
        marginBottom: 20,
      }),
      Span('Small text detail.', {
        fontSize: '0.9rem',
        color: '#999',
      }),
      Button('Learn More', {
        padding: '10px 20px',
        backgroundColor: 'dodgerblue',
        color: 'white',
        borderRadius: 5,
        cursor: 'pointer',
        onClick: () => alert('Hello MeoNode!'),
      }),
    ],
  }).render()

Container Nodes

Layout and container elements pass children via the children prop.

Applies to: Column, Row, Div, Section, Header, Footer, Main, Nav, Form, Ul, Ol

import { Column, Row, Button, Span, H1, Text } from '@meonode/ui'

const IconButton = () =>
  Button([Span('🚀', { marginRight: '8px' }), 'Launch App'], {
    padding: '10px 20px',
    backgroundColor: 'purple',
    color: 'white',
    borderRadius: '5px',
    onClick: () => console.log('Launching!'),
  }).render()

const Dashboard = () =>
  Column({
    padding: 30,
    backgroundColor: '#f0f0f0',
    borderRadius: '8px',
    boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
    children: [
      H1('Dashboard', {
        fontSize: '2.5rem',
        marginBottom: 15,
      }),
      Text('Container nodes use children prop for complex content.', {
        fontSize: '1rem',
        marginBottom: 20,
      }),
      Row({
        gap: 10,
        children: [
          Button('Action', {
            padding: '10px 20px',
            backgroundColor: 'dodgerblue',
            color: 'white',
            borderRadius: 5,
            cursor: 'pointer',
          }),
          IconButton(),
        ],
      }),
    ],
  }).render()

Key Points:

  • No JSX — Pure function composition
  • Type-Safe Styling — CSS properties as props with full autocomplete
  • Layout PrimitivesColumn (flex column), Row (flex row) for layouts

Event Handling

Standard React event handlers work directly:

import { Button } from '@meonode/ui'

const ClickableButton = () =>
  Button('Click Me', {
    onClick: () => console.log('Clicked!'),
    onMouseEnter: () => console.log('Hovered!'),
    backgroundColor: 'purple',
    color: 'white',
    padding: 10,
  }).render()

Styling Engine

Automatic CSS Processing

Props matching CSS property names are automatically processed by MeoNode's styling engine:

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

const StyledComponent = () =>
  Div({
    padding: '20px',
    backgroundColor: '#f0f0f0',
    borderRadius: '8px',
    boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
    children: Button('Submit', {
      padding: '10px 20px',
      backgroundColor: 'blue',
      minHeight: '40px',
      cursor: 'pointer',
    }),
  }).render()

Advanced Styling with css Prop

Use the css prop for pseudo-classes, media queries, and complex selectors:

import { Button } from '@meonode/ui'

const AdvancedButton = () =>
  Button('Hover Me', {
    padding: '12px 24px',
    backgroundColor: '#3B82F6',
    css: {
      '&:hover': {
        transform: 'scale(1.05)',
        boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
      },
      '@media (max-width: 768px)': {
        padding: '8px 16px',
      },
    },
  }).render()

Bypassing Style Processing

MeoNode's styling engine automatically processes any prop that matches a CSS property name. When you need to pass CSS-like prop names to components for non-styling purposes, use the props property:

import { Node, Component } from '@meonode/ui'
import { TextField } from '@mui/material'

// Custom MeoNode component that needs 'height' for logic
type ChartProps = {
  height: number // For chart dimensions, not styling
  data: number[]
}

const Chart = Component<ChartProps>(({ height, data }) =>
  Div({
    children: `Chart with ${data.length} points at ${height}px`,
  }),
)

// Usage with MeoNode components
const ChartView = () =>
  Node(Chart, {
    props: {
      height: 500, // Passed to Chart logic, not processed as CSS
    },
    data: [1, 2, 3, 4], // Non-CSS prop - stays at root level
    // These are processed as CSS for the container
    padding: '20px',
    backgroundColor: 'white',
  }).render()

// Usage with third-party components
const MyTextField = () =>
  Node(TextField, {
    props: {
      height: 100, // Passed directly to TextField without CSS processing
    },
    multiline: true,
    padding: '20px',
    backgroundColor: 'white',
  }).render()

Rules:

  • CSS property names (see full list) used for component logic → wrap in props: { }
  • Non-CSS prop names (data, title, onClick, isActive, etc.) → pass at root level
  • Styling props → always at root level

Use Cases:

  • Component-specific props that conflict with CSS property names
  • Direct prop passing to third-party components

For more styling patterns, see the Styling Guide.


Conditional Rendering

Use standard JavaScript conditionals:

import { Column, Text, Button } from '@meonode/ui'
import { useState } from 'react'

const AuthStatus = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false)

  return Column({
    children: isLoggedIn
      ? Text('Welcome back!', { color: 'green' })
      : Button('Login', {
          backgroundColor: 'blue',
          color: 'white',
          padding: 10,
          onClick: () => setIsLoggedIn(true),
        }),
  }).render()
}

Conditional Components with Hooks

Critical: Components using React Hooks must follow the Rules of Hooks. Direct conditional calls break hook ordering.

import { Node, Column, Text } from '@meonode/ui'
import { useEffect, useState } from 'react'

const DetailComponent = ({ info }: { info: string }) => {
  useEffect(() => console.log('Mounted:', info), [info])
  return Text(info, { padding: 8 }).render()
}

const SafeConditional = () => {
  const [showMore, setShowMore] = useState(true)

  return Column({
    children: [Node(DetailComponent, { info: 'Always visible' }), showMore && Node(DetailComponent, { info: 'Conditionally visible' })],
  }).render()
}

✅ Alternative: Inline Function Wrapper

const SafeInline = () => {
  const [showMore, setShowMore] = useState(true)

  return Column({
    children: [showMore && (() => DetailComponent({ info: 'Safe with inline wrapper' }))],
  }).render()
}

✅ Alternative: Component HOC

import { Component } from '@meonode/ui'

const DetailComponent = Component<{ info: string }>(({ info }) => {
  useEffect(() => console.log('Mounted:', info), [info])
  return Text(info, { padding: 8 }).render()
})

const SafeHOC = () => {
  const [showMore, setShowMore] = useState(true)

  return Column({
    children: [showMore && DetailComponent({ info: 'Safe with Component HOC' })],
  }).render()
}

❌ Unsafe: Direct Conditional Call

// ❌ Throws "Rendered fewer hooks than expected"
const UnsafeExample = () => {
  const [showMore, setShowMore] = useState(true)

  return Column({
    children: [showMore && DetailComponent({ info: 'Breaks hook rules' })],
  }).render()
}

Summary:

  • Prefer Node() — Most idiomatic MeoNode pattern
  • Inline functions or Component HOC — Valid alternatives
  • Never call hook-using components conditionally — Violates Rules of Hooks

Performance Optimization

MeoNode's memoization system prevents unnecessary re-renders by controlling when nodes update via dependency arrays.

Static Nodes

Empty dependency array ([]) creates a static node that renders once and never updates:

import { Div } from '@meonode/ui'
import { useState } from 'react'

const App = () => {
  const [count, setCount] = useState(0)

  return Div({
    onClick: () => setCount(count + 1),
    children: [
      Div({ children: `Reactive: ${count}` }), // Updates on every click
      Div({ children: `Static: ${count}` }, []), // Stays at 0
    ],
  }).render()
}

Dependency-Based Re-Renders

Nodes re-render only when specified dependencies change:

import { Div } from '@meonode/ui'
import { useState } from 'react'

const App = () => {
  const [count1, setCount1] = useState(0)
  const [count2, setCount2] = useState(0)

  return Div({
    children: [
      Div({ onClick: () => setCount1(c => c + 1), children: 'Increment 1' }),
      Div({ onClick: () => setCount2(c => c + 1), children: 'Increment 2' }),
      Div({ children: `Count 1: ${count1}` }, [count1]), // Only updates when count1 changes
      Div({ children: `Count 2: ${count2}` }, [count2]), // Only updates when count2 changes
    ],
  }).render()
}

Memoizing HOCs

Memoization works with Component factory:

import { Component, Div } from '@meonode/ui'
import { useState } from 'react'

const HocComp = Component(({ children }) => Div({ children }))

const App = () => {
  const [count, setCount] = useState(0)

  return Div({
    onClick: () => setCount(c => c + 1),
    children: [
      HocComp({ children: `Static: ${count}` }, []), // Never re-renders
    ],
  }).render()
}

Rendering Lists

Use Array.map() for list rendering:

import { Column, Text } from '@meonode/ui'

const ItemList = () => {
  const items = [
    { id: 1, text: 'First item' },
    { id: 2, text: 'Second item' },
    { id: 3, text: 'Third item' },
  ]

  return Column({
    children: items.map(item =>
      Text(item.text, {
        key: item.id,
        padding: 8,
        margin: 4,
        backgroundColor: '#e9e9e9',
        borderRadius: '4px',
      }),
    ),
  }).render()
}

JSX Component Integration

MeoNode seamlessly integrates with existing JSX components and libraries.

Basic Integration with Node()

import { Node, Column, H1 } from '@meonode/ui'
import { TextField } from '@mui/material'
import { DatePicker } from 'antd'

// JSX component
const Card = ({ children }: { children: React.ReactNode }) => (
  <div className = "card" > { children } < /div>
)

// MeoNode component (must call .render())
const MeoNodeComponent = () =>
  Column({
    padding: '20px',
    children: H1('Hello from MeoNode!')
  }).render()

const IntegratedApp = () =>
  Column({
    children: [
      H1('JSX Integration'),
      Node(Card, {
        children: 'JSX component works!'
      }),
      Node(MeoNodeComponent),
      Node(TextField, {
        label: 'Name',
        variant: 'outlined',
        fullWidth: true
      }),
      Node(DatePicker, {
        width: '100%', // CSS prop processed by MeoNode
        placeholder: 'Select date'
      })
    ]
  }).render()

ReactElement Requirement

All components used with Node() must return a ReactElement:

import { Column, H1, Node } from '@meonode/ui'

// ❌ Returns Node instance (missing .render())
const ComponentOne = () => Column({
  children: H1('Component One')
})

// ✅ Returns ReactElement (.render() called)
const ComponentTwo = () => Column({
  children: H1('Component Two')
}).render()

// ✅ JSX component (already returns ReactElement)
const JSXComponent = ({ children }: { children: React.ReactNode }) => (
  <div>{ children } < /div>
)

const App = () =>
  Column({
    children: [
      ComponentOne(), // ✅ Direct call in children array
      // Node(ComponentOne), // ❌ Would fail (returns Node, not ReactElement)
      Node(ComponentTwo), // ✅ Returns ReactElement
      Node(JSXComponent, { children: 'Hello!' }) // ✅ JSX returns ReactElement
    ]
  }).render()

Creating Reusable Node Factories

Use createNode() and createChildrenFirstNode() for frequently used components:

import { createNode, createChildrenFirstNode, Column } from '@meonode/ui'
import { TextField, Button as MuiButton } from '@mui/material'
import { DatePicker } from 'antd'

// Standard node factories
const StyledTextField = createNode(TextField, {
  variant: 'outlined',
  fullWidth: true,
})

const MyDatePicker = createNode(DatePicker)

// Children-first nodes (text-focused components)
const PrimaryButton = createChildrenFirstNode(MuiButton, {
  variant: 'contained',
  size: 'large',
})

const LoginForm = () =>
  Column({
    gap: 16,
    children: [
      StyledTextField({
        label: 'Username',
        type: 'text',
      }),
      StyledTextField({
        label: 'Password',
        type: 'password',
      }),
      MyDatePicker({
        placeholder: 'Birth Date',
      }),
      PrimaryButton('Login', {
        onClick: () => console.log('Logging in...'),
      }),
    ],
  }).render()

Pattern Selection:

  • Node() — One-off JSX component usage
  • createNode() — Complex components with many props (forms, containers)
  • createChildrenFirstNode() — Text-focused components (buttons, typography, links)

Creating Reusable Components

Use the Component factory for encapsulated, reusable components:

import { Component, Button, Column } from '@meonode/ui'

interface PrimaryButtonProps {
  onClick: () => void
  children: React.ReactNode
}

const PrimaryButton = Component<PrimaryButtonProps>(props =>
  Button(props.children, {
    padding: '12px 24px',
    backgroundColor: 'darkgreen',
    color: 'white',
    borderRadius: 8,
    cursor: 'pointer',
    fontSize: '1.1rem',
    ...props, // Pass down additional props
  }),
)

const MySection = () =>
  Column({
    children: [
      PrimaryButton({
        children: 'Submit',
        onClick: () => console.log('Submitted!'),
      }),
    ],
  }).render()

Prop Spreading

Always spread ...props onto the root node to ensure className, id, aria-*, and event handlers are correctly applied.


Complete Example

Production-grade Todo app demonstrating MeoNode patterns:

import { Root, Column, Row, H1, Text, Button, Input, Node } from '@meonode/ui'
import { useState } from 'react'
import React from 'react'

interface Todo {
  id: number
  text: string
  completed: boolean
}

const TodoItem: React.FC<{
  todo: Todo
  onToggle: (id: number) => void
  onDelete: (id: number) => void
}> = ({ todo, onToggle, onDelete }) =>
  Row({
    padding: 12,
    backgroundColor: todo.completed ? '#f0f8f0' : '#fff',
    borderRadius: 8,
    border: '1px solid #e0e0e0',
    alignItems: 'center',
    gap: 12,
    children: [
      Text(todo.text, {
        flex: 1,
        textDecoration: todo.completed ? 'line-through' : 'none',
        color: todo.completed ? '#666' : '#333',
      }),
      Button('Toggle', {
        padding: '6px 12px',
        backgroundColor: todo.completed ? '#28a745' : '#6c757d',
        color: 'white',
        borderRadius: 4,
        cursor: 'pointer',
        onClick: () => onToggle(todo.id),
      }),
      Button('Delete', {
        padding: '6px 12px',
        backgroundColor: '#dc3545',
        color: 'white',
        borderRadius: 4,
        cursor: 'pointer',
        onClick: () => onDelete(todo.id),
      }),
    ],
  }).render()

const TodoApp = () => {
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, text: 'Learn MeoNode UI', completed: false },
    { id: 2, text: 'Build awesome apps', completed: false },
  ])
  const [inputValue, setInputValue] = useState('')

  const addTodo = () => {
    if (inputValue.trim()) {
      setTodos([
        ...todos,
        {
          id: Date.now(),
          text: inputValue.trim(),
          completed: false,
        },
      ])
      setInputValue('')
    }
  }

  const toggleTodo = (id: number) => {
    setTodos(todos.map(todo => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)))
  }

  const deleteTodo = (id: number) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  return Column({
    padding: 40,
    maxWidth: 600,
    margin: '0 auto',
    children: [
      H1('MeoNode Todo App', {
        textAlign: 'center',
        color: '#333',
        marginBottom: 30,
      }),
      Row({
        gap: 12,
        marginBottom: 24,
        children: [
          Input({
            value: inputValue,
            onChange: e => setInputValue(e.target.value),
            placeholder: 'Add a new todo...',
            padding: '12px 16px',
            borderRadius: 8,
            border: '2px solid #e0e0e0',
            fontSize: '16px',
            flex: 1,
            onKeyPress: e => e.key === 'Enter' && addTodo(),
          }),
          Button('Add Todo', {
            padding: '12px 24px',
            backgroundColor: '#007bff',
            color: 'white',
            borderRadius: 8,
            fontSize: '16px',
            cursor: 'pointer',
            onClick: addTodo,
          }),
        ],
      }),
      Column({
        gap: 12,
        children:
          todos.length > 0
            ? todos.map(todo =>
                Node(TodoItem, {
                  key: todo.id,
                  todo,
                  onToggle: toggleTodo,
                  onDelete: deleteTodo,
                }),
              )
            : [
                Text('No todos yet. Add one above!', {
                  textAlign: 'center',
                  color: '#666',
                  fontStyle: 'italic',
                }),
              ],
      }),
    ],
  }).render()
}

export default TodoApp

Next Steps

  • Styling — Advanced patterns: pseudo-classes, media queries, animations
  • Theming — Design system setup, semantic tokens, context-based themes
  • Framework Integration — Next.js, Vite, Remix configuration
  • FAQ — Common patterns, troubleshooting, migration strategies
  • Release Notes — Changelog, breaking changes, upgrade guides

On this page