• Getting Started
    • Overview
    • Why Without JSX?
    • Installation
    • Usage
    • Styling
    • Theming
    • Portal System
    • Rules & Patterns
    • Framework Integration
    • FAQ
    • Release Notes
  • MUI Integration
  • Components
  • Hooks

Frequently Asked Questions — MeoNode UI

A collection of common questions and answers to help you get started quickly with MeoNode UI.

18 questions found

Quick Navigation

Common Questions

MeoNode UI is a powerful and opinionated React UI library designed to streamline the development of modern, scalable, and highly maintainable user interfaces. It champions a component-first architecture and type-safe design, offering a unique approach to building UIs that prioritizes structure, predictability, and developer experience.

At its heart, MeoNode UI allows you to define and compose your UI using declarative function calls and plain JavaScript objects, moving away from traditional JSX-heavy component definitions towards a more structured and functional paradigm.

Why MeoNode

  • No build step required — works with plain JavaScript/TypeScript, no JSX transform
  • Type-safe by design — full TypeScript inference and autocomplete on every prop
  • Structured architecture — function-call composition promotes consistent organization
  • Integrated theming — design tokens resolve directly inside style props
  • Performance-aware — efficient style generation and memoization built in

Who it's for

  • Teams building large-scale React apps where consistency matters
  • TypeScript-first developers who want type safety on UI structure, not just data
  • Projects that need a built-in theming system without extra libraries
  • Anyone who prefers function composition over JSX

Why MeoNode

  • No build step required — works with plain JavaScript/TypeScript, no JSX transform
  • Type-safe by design — full TypeScript inference and autocomplete on every prop
  • Structured architecture — function-call composition promotes consistent organization
  • Integrated theming — design tokens resolve directly inside style props
  • Performance-aware — efficient style generation and memoization built in

Who it's for

  • Teams building large-scale React apps where consistency matters
  • TypeScript-first developers who want type safety on UI structure, not just data
  • Projects that need a built-in theming system without extra libraries
  • Anyone who prefers function composition over JSX

Install the package and you're ready — no JSX transform, no build plugin.

npm install @meonode/ui

A minimal first component:

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

export default function App() {
  return Column({
    padding: 24,
    gap: 16,
    children: [
      H1('Hello, MeoNode'),
      Button('Click me', {
        padding: '10px 20px',
        backgroundColor: '#3B82F6',
        color: 'white',
        borderRadius: 6,
        onClick: () => alert('Hi'),
      }),
    ],
  }).render()
}

That's it — function calls instead of JSX, props instead of attributes. Children-first elements (H1, Button, Text) take content as the first argument; container elements (Column, Row, Div) take children via the children prop.

For theming, wrap your app in ThemeProvider and reference tokens by string ('var(--meonode-theme-primary)') or via useTheme(). See the Theming guide.

Install with your package manager of choice:

npm install @meonode/ui
# or
yarn add @meonode/ui
# or
bun add @meonode/ui

React and React-DOM are peer dependencies. No JSX transform, build plugin, or extra configuration is required.

Framework compatibility

MeoNode UI is a plain React library — it works anywhere React works:

  • Next.js — App Router and Pages Router, including Server Components
  • Vite — standard React templates work out of the box
  • Create React App — drop-in
  • Remix, Astro, etc. — any React-compatible host

Wrap any JSX/React component with Node() and use it like any MeoNode element. Props, hooks, refs, and event handlers all work unchanged.

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

const Form = Column({
  gap: 16,
  children: [
    H1('Sign up'),
    Node(TextField, { label: 'Name', variant: 'outlined' }),
    Node(DatePicker, { placeholder: 'Birth date', style: { width: '100%' } }),
    Button('Submit', { backgroundColor: 'green' }),
  ],
})

Key points

  • No conversion — JSX components work as-is
  • Props, hooks, refs, events all behave exactly like in JSX
  • CSS classes and inline styles are preserved
  • Mix freely — alternate JSX and MeoNode at any depth

Reusing the same component

If you'll use the same JSX component repeatedly with default props, reach for createNode() or createChildrenFirstNode() instead of repeating Node() calls. See Node() vs createNode() vs createChildrenFirstNode() for the comparison.

Gradual adoption

Start using MeoNode in new components while keeping existing JSX untouched. The two paradigms compose without friction, so there's no migration step.

These three functions serve different purposes for integrating JSX/React components into MeoNode UI:

Node()

Purpose: Quick one-off usage of JSX/React components

When to use: When you need to render an existing component immediately without creating a reusable wrapper.

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

const MyForm = Column({
  children: [Node(TextField, { label: 'Username', variant: 'outlined' }), Node(TextField, { label: 'Password', type: 'password' })],
})

createNode()

Purpose: Creates a reusable node factory with props-first pattern

Pattern: Single props argument where children must be passed as a prop

When to use: When you'll use a component multiple times and want to set default props

import { createNode } from '@meonode/ui'
import { TextField } from '@mui/material'

// Create reusable factory with default props
const StyledTextField = createNode(TextField, {
  variant: 'outlined',
  fullWidth: true,
})

// Usage - single props object
const MyForm = Column({
  children: [StyledTextField({ label: 'Username' }), StyledTextField({ label: 'Password', type: 'password' })],
})

createChildrenFirstNode()

Purpose: Creates a reusable node factory with children-first pattern

Pattern: Two arguments - children first, then props object

When to use: For text-focused components like buttons, headings, or typography where content is the primary focus

import { createChildrenFirstNode } from '@meonode/ui'
import { Button as MuiButton } from '@mui/material'

// Create reusable factory with default props
const PrimaryButton = createChildrenFirstNode(MuiButton, {
  variant: 'contained',
  color: 'primary',
  size: 'large',
})

// Usage - children first, props second
const MyForm = Column({
  children: [PrimaryButton('Login', { onClick: handleLogin }), PrimaryButton('Sign Up', { onClick: handleSignup })],
})

Quick Comparison

FunctionUsage PatternReusableDefault PropsBest For
Node()Node(Component, props)❌ No❌ NoOne-off usage
createNode()Factory({ prop, children })✅ Yes✅ YesProps-first components (inputs, pickers)
createChildrenFirstNode()Factory('content', { prop })✅ Yes✅ YesContent-first components (buttons, text)

Key Takeaway

  • Node(): Direct usage, no abstraction
  • createNode(): Reusable factory, 1 argument (props with children inside)
  • createChildrenFirstNode(): Reusable factory, 2 arguments (children, then props)

Next.js enforces strict signatures on Page components — they must match Next's PageProps type. The Component HOC produces a function whose props shape may not align with that constraint, which can surface as a TypeScript error at the page boundary.

Top-level pages should be standard React functions that call .render() on the returned MeoNode:

export default function HomePage() {
  return Column({
    children: [Text('Hello World')],
  }).render()
}

✅ Use Component for sub-components

The HOC is the right tool for reusable sub-components — just don't export the HOC-wrapped function as a Next.js page directly:

const MyButton = Component<{ text: string }>(props =>
  Button(props.text, { backgroundColor: 'blue', color: 'white' }),
)

export default function HomePage() {
  return Column({
    children: [Node(MyButton, { text: 'Click me!' })],
  }).render()
}

Why

  • Next.js page components must match its PageProps signature.
  • The Component HOC adds children and optional props overrides that Next.js doesn't expect at the page boundary.
  • Always call .render() at the page's top level so Next.js receives a real React element.

Hot Module Replacement (HMR) requires proper setup for MeoNode UI sub-components:

For Vite Projects:

  1. Use .tsx file extensions for all component files
  2. Wrap components with Node() in the parent component
// 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 return React Element is wrapped with Node()
    ],
  }).render()
}

For Next.js Projects:

Next.js is more intelligent with 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.

Node functions come in two shapes — matching the two factory functions, createNode and createChildrenFirstNode.

Children-first (H1, Button, Text, Span, …): first argument is the content, second is props.

H1('Welcome!', { fontSize: '2rem', color: '#333' })
Button('Click Me', { backgroundColor: 'blue', onClick: handleClick })

Props-first (Column, Row, Div, …): single props object, children passed via the children key.

Column({
  padding: 20,
  children: [H1('Title'), Text('Content')],
})

Use createChildrenFirstNode to build your own children-first wrappers and createNode for props-first ones.

Styling is done through props using a CSS-in-JS approach:

  • Raw numbers are interpreted as pixels (20 becomes 20px)
  • CSS properties are passed directly as props
  • Theme tokens like 'var(--meonode-theme-primary)' resolve via the active ThemeProvider
  • Type safety — autocomplete and error checking on every prop
Button('Submit', {
  padding: '10px 20px',
  backgroundColor: 'var(--meonode-theme-primary)',
  color: 'var(--meonode-theme-primary-content)',
  borderRadius: 5,
})

For dynamic styles, use the css prop with a theme function:

Div({
  css: {
    color: theme => var(--meonode-theme-system-primary-content),
    '&:hover': { backgroundColor: 'var(--meonode-theme-primary)' },
  },
})

How do I pass props like height to custom components?

When creating custom components in MeoNode UI, you need to understand how props are processed:

The Rule

  • CSS-like prop names (height, width, padding, margin, etc.) used for non-styling purposes → must go in the props object
  • Non-CSS prop names (data, title, onClick, etc.) → can be passed at root level

Why?

MeoNode UI processes CSS-like prop names as styles. If you have a custom component that needs height for logic (not styling), passing it directly will be intercepted by the style processor.

Example

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

type ChartProps = {
  height: number // For chart logic, not styling
  data: number[] // Not a CSS prop
}

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

// Usage with Node
Node(Chart, {
  backgroundColor: 'white', // Styling prop
  padding: '20px', // Styling prop
  data: [1, 2, 3, 4], // Root level - not CSS-like
  props: {
    // CSS-like custom props here
    height: 500, // Goes to component logic, not styling
  },
})

When to use props

Only use the props object when your custom component prop has the same name as a CSS property but serves a different purpose. Otherwise, pass props directly at root level.

Use the Component factory to create reusable React components:

interface PrimaryButtonProps {
  onClick: () => void;
}

const PrimaryButton = Component<PrimaryButtonProps>((props) => {
  return Button(props.children, {
    padding: '12px 24px',
    backgroundColor: 'darkgreen',
    color: 'white',
    borderRadius: 8,
    ...props // Spread other props
  });
});

Always spread ...props to ensure additional properties are correctly applied.

The "Rendered fewer hooks than expected" (or similar) error comes from violating the Rules of Hooks. React requires that hooks like useState and useEffect are called in the same order on every render. If a component that uses hooks is only sometimes called (e.g. inside a conditional), the order of hook calls changes, and React throws this error.


Example: DetailComponent

In this guide we use DetailComponent as an example.
It returns a Node instance and uses hooks internally:

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

  return Row({
    alignItems: 'center',
    gap: 10,
    padding: 4,
    border: '2px solid var(--meonode-theme-accent)',
    borderRadius: 6,
    backgroundColor: 'var(--meonode-theme-warning)',
    color: 'var(--meonode-theme-danger)',
    children: [P(info), TextField({ sx: { background: 'var(--meonode-theme-primary)' } })],
  }).render()
}

Because it calls useEffect, this component must always be rendered consistently.
Here are the safe and unsafe ways to do it:


✅ Correct Pattern: Wrap with Node()

The most idiomatic MeoNode pattern is to wrap hook-using Node components with the Node() HOC.
This makes them safe to conditionally render, because React now sees a proper component boundary.

isShowMore &&
  Node(DetailComponent, {
    info: 'Safe: Node() wrapper ensures hook order is preserved.',
  })

✅ Correct Pattern: Inline Function Wrapper

Alternatively, you can wrap the call in an arrow function.
This delays execution until render time and avoids direct conditional hook calls.

isShowMore &&
  (() =>
    DetailComponent({
      info: 'Safe: inline function wrapper.',
    }))

This also works if the component explicitly calls .render() internally (e.g. ReturnRenderedDetailComponent).


✅ Correct Pattern: Component HOC

For components wrapped in the Component HOC, React handles hooks as usual.
This is another safe way to render them conditionally.

const WrappedDetailComponent = Component(DetailComponent)

isShowMore &&
  WrappedDetailComponent({
    info: 'Safe: Component HOC ensures hook order + theme context.',
  })

❌ Incorrect Pattern: Direct Conditional Call

Directly calling a hook-using component inside a conditional breaks the Rules of Hooks.
React sees a different hook order on renders and throws.

// ❌ Will throw "Rendered fewer hooks than expected"
isShowMore &&
  DetailComponent({
    info: 'This violates hook rules.',
  })

Summary

  • Node() is the recommended MeoNode-safe wrapper for conditional rendering.
  • Inline functions and the Component HOC are also safe, but use them when you need ReactNode compatibility or full theme propagation.
  • Never call a hook-using component directly inside a conditional — that’s what triggers the error.

Standard React event handlers work exactly as expected. Attach them as properties in the props object:

const handleClick = () => {
  console.log('Button clicked!')
}

const MyButton = Button('Click Me', {
  onClick: handleClick,
  onMouseEnter: () => console.log('Hover!'),
  backgroundColor: 'purple',
})

Conditional Rendering:

const isLoggedIn = true

const AuthStatus = Column({
  children: isLoggedIn ? Text('Welcome back!', { color: 'green' }) : Button('Login', { backgroundColor: 'blue' }),
})

List Rendering:

const items = [{ id: 1, text: 'Item 1' }]

const ItemList = Column({
  children: items.map(item =>
    Text(item.text, {
      key: item.id,
      padding: 8,
      backgroundColor: '#e9e9e9',
    }),
  ),
})

MeoNode UI provides several layout components:

  • Column: Vertical layout (flexDirection: column)
  • Row: Horizontal layout (flexDirection: row)
  • Div: Generic container
  • Section, Header, Footer, Main, Nav, Form: Semantic HTML containers

All layout components support flexbox properties and theme-aware styling.

Most standard HTML tags are also prebuilt as components, so you can use them directly in your UI code.

MeoNode UI integrates a robust theming system directly into styling:

  • Define design tokens once and use them across components
  • Theme-aware properties like var(--meonode-theme-colors-primary), var(--meonode-theme-spacing-md)
  • Global style changes through centralized theme configuration
  • Consistent visual language across your entire application

Augment the MeoTheme interface to get autocomplete and compile-time validation on every 'theme.<path>' string:

declare module '@meonode/ui' {
  interface MeoTheme {
    mode: 'light' | 'dark'
    system: {
      colors: { primary: string; bg: string }
      spacing: { sm: number; md: number }
    }
  }
}

See the Theming guide for the full setup.