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

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:

  1. Use .tsx file extensions for all component files
  2. Wrap components with Node() in the parent component
  3. 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 .ts or .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
  • .tsx extensions 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:

FactoryUse CaseExample
Node()One-off usage, JSX integrationNode(TextField, { ... })
createNode()Reusable complex/container componentsconst MyInput = createNode(Input)
createChildrenFirstNode()Reusable text-heavy componentsconst MyBtn = createChildrenFirstNode(Button)
ComponentEncapsulated 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