This guide will walk you through the fundamental principles and common patterns for building user interfaces with @meonode/ui
. Unlike traditional React development that heavily relies on JSX, @meonode/ui
embraces a component-first architecture and type-safe composition using declarative function calls.
@meonode/ui
Way: Nodes as Building BlocksAt the core of @meonode/ui
are node functions (e.g., Column
, H1
, Button
, Text
, Span
, etc.). These functions represent HTML elements or custom components and serve as the building blocks for your UI.
Each node function accepts arguments based on its primary purpose and the type of content it typically holds.
For HTML elements whose primary content is often text or a simple string, such as headings, paragraphs, simple text spans, or buttons with text labels, their first argument is reserved for their children. The second argument is for their props (including styles).
Examples: H1
, H2
, H3
, H4
, H5
, H6
, Text
, Span
(when containing just text), and Button
(when its content is primarily text).
import { Root, H1, Span, Button, Column, Text, Node } from '@meonode/ui'; const WelcomeSection = () => { const handleLearnMore = () => { alert('Hello from Meonode!'); }; return Column({ padding: 20, // Raw numbers like 20 are interpreted as pixels (e.g., 20px) children: [ H1('Welcome to our App!', { // Text as first argument for H1 fontSize: '2.5rem', color: '#333', marginBottom: 15 }), Text('This is a paragraph demonstrating how to use typography nodes.', { fontSize: '1rem', color: '#666', lineHeight: '1.5', marginBottom: 20 }), Span('This is a small span of text.', { fontSize: '0.9rem', color: '#999', marginBottom: 20 }), Button('Learn More', { // Text as first argument for Button padding: '10px 20px', backgroundColor: 'dodgerblue', color: 'white', borderRadius: 5, border: 'none', cursor: 'pointer', onClick: handleLearnMore }) ] }).render(); }; const App = () => { return Root({ children: Node(WelcomeSection) }).render(); };
children
Prop)For HTML elements that are typically used as containers for other elements, or interactive elements that might contain complex, varied content, their children are passed via the children
property within the second argument (the props object).
Examples: Column
, Row
, Div
, Section
, Header
, Footer
, Main
, Nav
, Form
, Img
, Input
, Ul
, Ol
, and Button
(if its children are other nodes).
import { Root, Column, Button, Span, H1, Text, Row, Node } from '@meonode/ui'; const IconButton = () => { const handleLaunch = () => { console.log('Launching!'); }; return Button([ Span('🚀', { marginRight: '8px' }), // An emoji as a child Span 'Launch App' // Text as another child ], { padding: '10px 20px', backgroundColor: 'purple', color: 'white', borderRadius: '5px', onClick: handleLaunch }).render(); }; const WelcomeSection = () => { const handleMoreInfo = () => { alert('Info fetched!'); }; return Column({ padding: 30, backgroundColor: '#f0f0f0', borderRadius: '8px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', children: [ H1('Welcome to our App!', { fontSize: '2.5rem', color: '#333', marginBottom: 15 }), Text('This is a paragraph demonstrating how to use container nodes.', { fontSize: '1rem', color: '#666', lineHeight: '1.5', marginBottom: 20 }), Row({ gap: 10, children: [ Button('Click for More Info', { padding: '10px 20px', backgroundColor: 'dodgerblue', color: 'white', borderRadius: 5, border: 'none', cursor: 'pointer', onClick: handleMoreInfo }), Node(IconButton) ] }) ] }).render(); }; const App = () => { return Root({ children: Node(WelcomeSection) }).render(); };
Key Takeaways:
< />
). You're calling functions that return nodes.Column
(default flexDirection: column
) and Row
(default flexDirection: row
) are key for organizing content with flexbox.MeoNode UI seamlessly integrates with existing JSX components and libraries, allowing for gradual adoption and interoperability.
Node()
Use the Node()
function to integrate any existing JSX component:
import { Root, Node, Column, H1 } from '@meonode/ui'; import { DatePicker } from 'antd'; import { TextField } from '@mui/material'; // Existing JSX component const MyJSXComponent = (props) => <div className="card">{props.children}</div>; // MeoNode function component - must call .render() const MyMeoNodeComponent = () => Column({ padding: '20px', children: H1('Hello from MeoNode!') }).render(); // ✅ Call .render() to return React element const IntegratedApp = () => { return Column({ children: [ H1('MeoNode with JSX Integration'), Node(MyJSXComponent, { children: 'This JSX component works perfectly!' }), Node(MyMeoNodeComponent), // ✅ Function returns React element Node(TextField, { label: 'Name', variant: 'outlined', fullWidth: true }), Node(DatePicker, { width: '100%', // Direct CSS prop - processed by MeoNode placeholder: 'Select date' }) ] }).render(); }; const App = () => { return Root({ children: Node(IntegratedApp) }).render(); };
All components used with Node()
, createNode()
, or createChildrenFirstNode()
must return a ReactElement:
import { Root, Column, H1, Node } from '@meonode/ui'; // Component returns Node instance, MeoNode function without .render() const ComponentOne = () => Column({ children: H1('This is component one!') }); // Missing .render() // Component returns React Element, MeoNode function with .render() const ComponentTwo = () => Column({ children: H1('This is component two!') }).render(); // Call .render() to compile to React element // JSX component (already returns ReactElement) const JSXComponent = (props) => <div>{props.children}</div>; // Usage with Node() const App = () => { return Root({ children: Column({ children: [ ComponentOne(), // ✅ This works because it's called directly and does not contain hooks // Node(WrongComponent), // ❌ This would cause issues Node(ComponentTwo), // ✅ This works Node(JSXComponent, { children: 'Hello!' }), // ✅ This works ] }) }).render(); };
For components you'll use frequently, create node factories using createNode()
and createChildrenFirstNode()
:
import { Root, createNode, createChildrenFirstNode, Column, Node } from '@meonode/ui'; import { TextField, Button as MuiButton } from '@mui/material'; import { DatePicker } from 'antd'; // Create standard node factories const StyledTextField = createNode(TextField, { variant: 'outlined', fullWidth: true }); const MyDatePicker = createNode(DatePicker); // Create children-first nodes (for typography, buttons, text-focused components) const PrimaryButton = createChildrenFirstNode(MuiButton, { variant: 'contained', size: 'large' }); // Usage follows MeoNode patterns const LoginForm = () => { const handleLogin = () => { console.log('Logging in...'); }; return Column({ gap: 16, children: [ StyledTextField({ label: 'Username', type: 'text' }), StyledTextField({ label: 'Password', type: 'password' }), MyDatePicker({ placeholder: 'Birth Date' }), // Children-first pattern PrimaryButton('Login', { onClick: handleLogin }) ] }).render(); }; const App = () => { return Root({ children: Node(LoginForm) }).render(); };
When to Use Each Pattern:
Node()
- Quick one-off usage of JSX componentscreateNode()
- For complex components with multiple props (forms, containers, layout components)createChildrenFirstNode()
- For typography, buttons, links, or any component where text/content is the primary focusComponent
For reusability and encapsulation, @meonode/ui
provides the Component
factory. This allows you to define a standard React component that internally uses @meonode/ui
nodes.
import { Root, Component, Button, Column, Node } from '@meonode/ui'; interface PrimaryButtonProps { onClick: () => void; children: React.ReactNode; // 'children' prop is automatically merged, no need to specify } const PrimaryButton = Component<PrimaryButtonProps>((props) => { return Button(props.children, { padding: '12px 24px', backgroundColor: 'darkgreen', color: 'white', borderRadius: 8, border: 'none', cursor: 'pointer', fontSize: '1.1rem', ...props // Pass down other props (like onClick) }); }); // Usage const MySection = () => { const handleSubmit = () => { console.log('Data Submitted!'); }; return Column({ children: [ PrimaryButton({ children: 'Submit Data', onClick: handleSubmit }) ] }).render(); }; const App = () => { return Root({ children: Node(MySection) }).render(); };
...props
)When creating reusable components, always spread ...props
onto the root node. This ensures that additional properties like className
, id
, aria-label
, or event handlers are correctly applied to the underlying HTML element.
MeoNode UI features a powerful CSS-first styling engine that automatically processes CSS properties:
Any prop that matches a CSS property name is automatically processed by MeoNode's styling engine:
import { Root, Button, Div, Node } from '@meonode/ui'; const StyledComponent = () => { return Div({ padding: '20px', // Processed as CSS backgroundColor: '#f0f0f0', // Processed as CSS borderRadius: '8px', // Processed as CSS boxShadow: '0 2px 4px rgba(0,0,0,0.1)', // Processed as CSS children: Button('Styled Button', { padding: '10px 20px', // Processed as CSS backgroundColor: 'blue', // Processed as CSS minHeight: '40px', // Processed as CSS cursor: 'pointer' // Processed as CSS }) }).render(); }; const App = () => { return Root({ children: Node(StyledComponent) }).render(); };
css
PropThe css
prop is automatically available for all MeoNode components, allowing complex styling:
import { Root, Button, Node } from '@meonode/ui'; const AdvancedButton = () => { return Button('Hover Me', { padding: '12px 24px', // Direct CSS prop backgroundColor: '#3B82F6', // Direct CSS prop css: { // Advanced styling '&:hover': { transform: 'scale(1.05)', boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)' }, '@media (max-width: 768px)': { padding: '8px 16px' } } }).render(); }; const App = () => { return Root({ children: Node(AdvancedButton) }).render(); };
When you need to pass props directly to the underlying component without style processing, use the props
property:
import { Root, Node } from '@meonode/ui'; const MyComponent = () => { // ❌ This minHeight will be processed as CSS const incorrectUsage = SomeComponent({ minHeight: 10, // Treated as CSS: min-height: 10px maxWidth: '500px' // Treated as CSS: max-width: 500px }); // ✅ Use 'props' to bypass style processing return SomeComponent({ props: { minHeight: 10, // Passed directly to component as prop maxWidth: '500px', // Passed directly to component as prop }, customProp: 'value', // Passed directly to component as prop // These are still processed as CSS padding: '20px', backgroundColor: 'white' }).render(); }; const App = () => { return Root({ children: Node(MyComponent) }).render(); };
When integrating JSX components, the styling engine automatically handles CSS properties:
import { Root, Node, Column, createNode } from '@meonode/ui'; import { TextField } from '@mui/material'; // JSX component wrapped with Node() gets automatic CSS processing const IntegratedForm = () => { return Column({ children: [ Node(TextField, { padding: '16px', // Processed as CSS borderRadius: '8px', // Processed as CSS // TextField-specific properties label: 'Name', variant: 'outlined', fullWidth: true }) ] }).render(); }; // Factory-created components also get automatic CSS processing const StyledTextField = createNode(TextField, { variant: 'outlined', // Initial prop fullWidth: true // Initial prop }); const MyForm = () => { return Column({ children: [ StyledTextField({ margin: '8px 0', // Processed as CSS backgroundColor: '#f9f9f9', // Processed as CSS label: 'Username', // Passed to TextField type: 'text' // Passed to TextField }) ] }).render(); }; const App = () => { return Root({ children: Node(MyForm) }).render(); };
For advanced styling patterns including pseudo-classes, media queries, and animations, see the Styling Guide.
One of the significant advantages of @meonode/ui
is its deep integration with TypeScript:
Standard React event handlers work exactly as expected:
import { Root, Button, Node } from '@meonode/ui'; const ClickableButton = () => { const handleClick = () => { console.log('Button clicked!'); }; const handleMouseEnter = () => { console.log('Hovered!'); }; return Button('Click Me', { onClick: handleClick, onMouseEnter: handleMouseEnter, backgroundColor: 'purple', color: 'white', padding: 10 }).render(); }; const App = () => { return Root({ children: Node(ClickableButton) }).render(); };
MeoNode uses standard JavaScript logic for conditional rendering, which allows you to display different content based on a condition.
import { Root, Column, Text, Button, Node } from '@meonode/ui'; const AuthStatus = () => { const [isLoggedIn, setIsLoggedIn] = useState(true); const handleLogin = () => { setIsLoggedIn(true); }; return Column({ children: isLoggedIn ? Text('Welcome back, user!', { color: 'green' }) : Button('Login / Register', { backgroundColor: 'blue', color: 'white', padding: 10, onClick: handleLogin }) }).render(); }; const App = () => { return Root({ children: Node(AuthStatus) }).render(); };
The example above shows how to render either a Text component or a Button component depending on the value of the isLoggedIn
state.
When conditionally rendering components that use React Hooks like useState
or useEffect
, you must strictly follow the Rules of Hooks to prevent runtime errors. Calling a hook-using component directly inside a conditional can lead to a "Rendered fewer hooks than expected"
error.
Node()
(Recommended)The most idiomatic way to handle this in MeoNode is by wrapping your hook-using components with the Node()
function. This creates a proper React component boundary and ensures the order of hooks is preserved, which is crucial for React's internal state management.
import { Root, Node, Column, Text } from '@meonode/ui'; import { useEffect, useState } from 'react'; const MyDetailComponent = ({ info }) => { useEffect(() => console.log('Mounted:', info), [info]); return Text(info, { padding: 8, backgroundColor: '#F9A825' }).render(); }; const ConditionalExample = () => { const [isShowMore, setIsShowMore] = useState(true); return Column({ children: [ Node(MyDetailComponent, { info: 'Safe with Node() wrapper' }), isShowMore && Node(MyDetailComponent, { info: 'Conditionally safe with Node()' }) ] }).render(); }; const App = () => { return Root({ children: Node(ConditionalExample) }).render(); };
Another safe approach is to use an inline arrow function (uncalled). This delays the execution of the component until MeoNode internally processes it with Node()
, which prevents it from breaking the hook order.
import { Root, Column, Node } from '@meonode/ui'; import { useState } from 'react'; const SafeConditionalExample = () => { const [isShowMore, setIsShowMore] = useState(true); return Column({ children: [ isShowMore && (() => MyDetailComponent({ info: 'Safe with inline function wrapper' })) ] }).render(); }; const App = () => { return Root({ children: Node(SafeConditionalExample) }).render(); };
For reusable components created with the Component factory, you can conditionally render them directly without an additional wrapper.
import { Root, Component, Column, Node } from '@meonode/ui'; import { useState } from 'react'; const WrappedDetailComponent = Component(MyDetailComponent); const ComponentHOCExample = () => { const [isShowMore, setIsShowMore] = useState(true); return Column({ children: [ isShowMore && WrappedDetailComponent({ info: 'Safe with Component HOC' }) ] }).render(); }; const App = () => { return Root({ children: Node(ComponentHOCExample) }).render(); };
You should never call a hook-using component directly inside a conditional expression. This violates the Rules of Hooks and will result in an error.
// ❌ This will throw "Rendered fewer hooks than expected" const UnsafeExample = () => { const [isShowMore, setIsShowMore] = useState(true); return Column({ children: [ isShowMore && MyDetailComponent({ info: 'Unsafe — breaks hook rules' }) ] }).render(); };
Node()
: It's the most recommended and idiomatic MeoNode solution.Use JavaScript's Array.prototype.map()
for list rendering:
import { Root, Column, Text, Node } 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, // Optional: custom key for React reconciliation padding: 8, margin: 4, backgroundColor: '#e9e9e9', borderRadius: '4px' }) ) }).render(); }; const App = () => { return Root({ children: Node(ItemList) }).render(); };
Here's a comprehensive example showing how to structure a real MeoNode application:
import { Root, Column, Row, H1, Text, Button, Input, Node } from '@meonode/ui'; import { useState } from 'react'; const TodoItem = ({ todo, onToggle, onDelete }) => { return 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, border: 'none', cursor: 'pointer', onClick: () => onToggle(todo.id) }), Button('Delete', { padding: '6px 12px', backgroundColor: '#dc3545', color: 'white', borderRadius: 4, border: 'none', cursor: 'pointer', onClick: () => onDelete(todo.id) }) ] }).render(); }; const TodoApp = () => { const [todos, setTodos] = useState([ { 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) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }; const deleteTodo = (id) => { 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, border: 'none', 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(); }; const App = () => { return Root({ children: Node(TodoApp) }).render(); }; export default App;
You've now grasped the fundamentals of building with @meonode/ui
. To explore more advanced features:
css
prop, pseudo-classes, media queries, and animations.@meonode/ui
with frameworks like Next.js, Vite, etc.