Build type-safe React UIs using function composition instead of JSX. This guide covers core patterns, component
architecture, and integration strategies for @meonode/ui.
MeoNode uses node functions (Column, H1, Button, Text, etc.) as building blocks. Each represents an HTML
element or component with type-safe props.
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: H1–H6, 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()
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:
Column (flex column), Row (flex row) for layoutsStandard 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()
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()
css PropUse 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()
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:
props: { }Use Cases:
For more styling patterns, see the Styling Guide.
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() }
Critical: Components using React Hooks must follow the Rules of Hooks. Direct conditional calls break hook ordering.
Node()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() }
const SafeInline = () => { const [showMore, setShowMore] = useState(true) return Column({ children: [showMore && (() => DetailComponent({ info: 'Safe with inline wrapper' }))], }).render() }
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() }
// ❌ Throws "Rendered fewer hooks than expected" const UnsafeExample = () => { const [showMore, setShowMore] = useState(true) return Column({ children: [showMore && DetailComponent({ info: 'Breaks hook rules' })], }).render() }
Summary:
Node() — Most idiomatic MeoNode patternMeoNode's memoization system prevents unnecessary re-renders by controlling when nodes update via dependency arrays.
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() }
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() }
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() }
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() }
MeoNode seamlessly integrates with existing JSX components and libraries.
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()
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()
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 usagecreateNode() — Complex components with many props (forms, containers)createChildrenFirstNode() — Text-focused components (buttons, typography, links)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()
Always spread ...props onto the root node to ensure className, id, aria-*, and event handlers are correctly
applied.
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
On this page