Rules & Patterns — MeoNode UI
A consolidated guide to the essential rules, best practices, and common patterns for building robust applications with
@meonode/ui.
Golden Rules
1. No JSX
MeoNode UI is designed to be used without JSX. Use the provided node functions (Div, Button, Text, etc.) to
compose your UI.
❌ Incorrect:
// Don't mix JSX with MeoNode const MyComponent = () => <div>Hello</div>
✅ Correct:
import { Div } from '@meonode/ui' const MyComponent = () => Div({ children: 'Hello' }).render()
2. Node() Requires ReactElement
When using Node() to render a component, that component must return a ReactElement. For MeoNode components, this
means calling .render().
❌ Incorrect:
// Returns Node instance, not ReactElement const MyComp = () => Div({ children: 'Hi' }) Node(MyComp) // Fails
✅ Correct:
// Returns ReactElement const MyComp = () => Div({ children: 'Hi' }).render() Node(MyComp) // Works
3. Do NOT Wrap MeoNode Exports
MeoNode's built-in components (Div, Button, ThemeProvider, Html, Body, etc.) are already factories. You
must call them directly. Never wrap them in Node().
❌ Incorrect:
Node(ThemeProvider, { theme: myTheme }) // Double wrapping! Node(Div, { children: 'Hi' }) // Incorrect
✅ Correct:
ThemeProvider({ theme: myTheme }) // Correct Div({ children: 'Hi' }) // Correct
4. The Rules of Hooks
Components using React Hooks must follow the Rules of Hooks. * Never call hook-using components conditionally.*
Why?
React relies on the order of hooks remaining constant between renders. When you call a function component directly (
e.g., MyComponent()) inside a conditional statement, you risk changing the number of hooks called, which causes React
to throw the "Rendered fewer hooks than expected" error.
❌ The Problem: Direct Conditional Calls
Calling a component function directly inside a conditional block breaks the hook order if the condition changes.
Consider this component that uses a hook:
const DetailComponent = ({ id }) => { const [data, setData] = useState(null) // ⚠️ Uses a hook! return Div({ children: `Details for ${id}` }).render() }
If we call it conditionally:
// ❌ Incorrect const App = () => { const [show, setShow] = useState(true) return Column({ children: [ // If 'show' becomes false, DetailComponent's hooks are skipped! // This crashes the entire app. show && DetailComponent({ id: 1 }) ] }).render() }
✅ The Solutions
Option A: Wrap in Node() (Recommended)
The Node() factory creates a React Element. React treats this as a proper component instance, managing its lifecycle
and hooks independently of the parent's condition.
show && Node(DetailComponent, { id: 1 })
Option B: Inline Function Wrapper
Wrapping the call in an arrow function () => ... delays execution. React treats this function as a component, ensuring
hooks are called within their own context.
show && (() => DetailComponent({ id: 1 }))
Option C: Component HOC
The Component factory creates a higher-order component that safely handles props and hooks.
const SafeComponent = Component(DetailComponent) show && SafeComponent({ id: 1 })
Best Practices
1. Top-Level .render() Only
In MeoNode, only the top-level node of a component should call .render(). Child nodes within the children array
are automatically rendered by the parent node.
Calling .render() on every child is unnecessary and adds visual noise, though it is technically valid.
❌ Less Ideal (Noisy):
const MyComponent = () => Column({ children: [ H1('Title').render(), // Unnecessary .render() Text('Subtitle').render(), // Unnecessary .render() Button('Click').render(), // Unnecessary .render() ], }).render()
✅ Best Practice (Clean):
const MyComponent = () => Column({ children: [ H1('Title'), // Clean Text('Subtitle'), // Clean Button('Click'), // Clean ], }).render() // Only top-level needs .render()
2. When to use .render() on children?
The only time you must call .render() on a child is when passing it to a custom prop that explicitly expects a
ReactElement (JSX.Element), rather than a MeoNode instance.
// 'icon' prop expects a ReactElement, not a Node Button('Settings', { icon: Node(SettingsIcon).render(), onClick: () => { } })
3. Hot Module Replacement (HMR) Setup
Proper setup ensures MeoNode UI sub-components support Hot Module Replacement for a smooth development experience.
For Vite Projects
Vite requires explicit patterns to enable HMR for MeoNode components:
- Use
.tsxfile extensions for all component files - Wrap components with
Node()in the parent component - Call
.render()in the component to return a ReactElement
Example:
// AnotherComponent.tsx import { Column, Text } from '@meonode/ui' export function AnotherComponent() { return Column({ children: [Text('Hello from sub-component!')], }).render() // ✅ Call .render() to return React Element } // page.tsx import { Column, Node } from '@meonode/ui' import { AnotherComponent } from './AnotherComponent' export default function Page() { return Column({ children: [ Node(AnotherComponent), // ✅ Function that returns React Element is wrapped with Node() ], }).render() }
For Next.js Projects
Next.js has more intelligent HMR detection:
- File extensions can be
.tsor.tsx Node()wrapping is still recommended but not always required- Next.js automatically handles most HMR scenarios
Why This Works
Node()wrapper ensures React can properly track component boundaries.tsxextensions help Vite's HMR system recognize React components- Proper module boundaries allow hot reloading to work correctly
Without these patterns, changes to sub-components may require full page refreshes instead of hot updates.
Styling Patterns
1. CSS Props at Root
Pass CSS properties directly to the component's root props. MeoNode's styling engine automatically detects and processes them.
Div({ backgroundColor: 'red', // CSS prop padding: 20, // CSS prop borderRadius: 8 // CSS prop })
2. Non-CSS Props at Root
Pass standard DOM attributes (onClick, id, aria-label) and custom logic props at the root level, provided they
don't clash with CSS property names.
Div({ onClick: handleClick, // DOM attribute id: 'my-div', // DOM attribute 'aria-label': 'Label', // DOM attribute isActive: true // Custom prop (ignored by CSS engine) })
3. Bypassing Style Engine
If you have a custom prop that shares a name with a CSS property (e.g., height, width, color) but is intended for
logic, wrap it in props: {}.
// 'height' is used for logic, not styling Node(Chart, { props: { height: 500 // Passed raw to component }, padding: 20 // Processed as CSS })
Performance Patterns
1. Static Nodes
For content that never changes, pass an empty dependency array []. This skips re-renders entirely after the first
mount.
Div({ children: 'I never change' }, [])
2. Surgical Memoization
Pass specific dependencies to control exactly when a node re-renders.
Div({ children: `Count: ${count}` }, [count]) // Only re-renders when 'count' changes
Component Factories
Choose the right factory for your needs:
| Factory | Use Case | Example |
|---|---|---|
Node() | One-off usage, JSX integration | Node(TextField, { ... }) |
createNode() | Reusable complex/container components | const MyInput = createNode(Input) |
createChildrenFirstNode() | Reusable text-heavy components | const MyBtn = createChildrenFirstNode(Button) |
Component | Encapsulated logic + UI (HOC) | const UserCard = Component(props => ...) |
FAQ & Troubleshooting
Q: Why am I getting "Rendered fewer hooks than expected"?
A: You are likely calling a component that uses hooks inside a conditional statement (e.g., cond && MyComp()). Wrap it
in Node(MyComp) or use an inline function () => MyComp().
Q: Why isn't my style being applied?
A: Ensure you are passing the style prop at the root level. If you are wrapping a custom component, make sure that
component spreads ...props to its root element.
Q: How do I pass className?
A: You generally don't need to. MeoNode handles class generation. If you must, pass it as a regular prop, but be aware
it might conflict with internal styling if not handled carefully.
On this page
- Rules & Patterns — MeoNode UI