Frequently Asked Questions — MeoNode UI
A collection of common questions and answers to help you get started quickly with MeoNode UI.
19 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
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, so rendering works anywhere React does. Server-rendered styling is the one framework-specific piece:
- 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, other React hosts — the client entrypoints are plain React and
work anywhere React does, and so does server-rendered styling: a page rendered
with React's
renderToStringorrenderToPipeableStreamcarries its styles in the HTML and hydrates with the same classes. The style registry (@meonode/ui/nextjs-registry) is only for the Next.js App Router. Since 3.1.0; before, a component styled withcsshydrated with a different class outside Next.js.
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 { Column, 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 { Column, 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
| Function | Usage Pattern | Reusable | Default Props | Best For |
|---|---|---|---|---|
Node() | Node(Component, props) | ❌ No | ❌ No | One-off usage |
createNode() | Factory({ prop, children }) | ✅ Yes | ✅ Yes | Props-first components (inputs, pickers) |
createChildrenFirstNode() | Factory('content', { prop }) | ✅ Yes | ✅ Yes | Content-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.
✅ Recommended: plain function for pages
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 }>(function MyButton(props) { return 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
PagePropssignature. - The
ComponentHOC addschildrenand optionalpropsoverrides 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:
- Use .tsx file extensions for all component files
- 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 a React element }
page.tsx
import { Column, Node } from '@meonode/ui' import { AnotherComponent } from './AnotherComponent' export default function Page() { return Column({ children: [ Node(AnotherComponent), // ✅ A function returning a 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 (
20becomes20px) - CSS properties are passed directly as props
- Theme tokens like
'theme.primary'resolve via the activeThemeProvider - 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 => theme.system.primary.content, '&:hover': { backgroundColor: 'var(--meonode-theme-primary)' }, }, })
To render a styled node as a different HTML tag (e.g. a styled Div as <a>), use the as prop — see the Styling guide.
Use the as prop on any styled node factory to render a different intrinsic HTML tag while keeping the same styles and Emotion class:
import { Div } from '@meonode/ui' Div({ as: 'a', href: '/home', backgroundColor: 'var(--meonode-theme-primary)', padding: '12px 24px', children: 'Go home', })
Works on Node(), createNode(), and createChildrenFirstNode(). TypeScript narrows DOM props and event handlers to the resolved tag (href on as: 'a', etc.).
Limitations:
- Custom React components cannot be used as
astargets — wrap withNode(MyComponent, …)instead. - No-style tags (e.g.
Script) do not supportas. asis stripped at render time and never leaks to the DOM.
Full guide: Styling → Element Polymorphism (as)
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
propsobject - 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 type { CSSProperties } from 'react' import { Component, Div, Node } from '@meonode/ui' type ChartProps = { height: number // For chart logic, not styling data: number[] // Not a CSS prop style?: CSSProperties // Opts the component into top-level CSS props } const Chart = Component<ChartProps>(function Chart({ height, data }) { return 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:
import { Button, Component } from '@meonode/ui' interface PrimaryButtonProps { onClick: () => void } const PrimaryButton = Component<PrimaryButtonProps>(function PrimaryButton({ children, onClick }) { return Button(children, { padding: '12px 24px', backgroundColor: 'darkgreen', color: 'white', borderRadius: 8, onClick, }) })
Destructure the props you need and forward them explicitly. Spreading the whole props object into a node's props
(...props) does not typecheck: the component's props include MeoNode's own reserved keys — children, key, and the
props shield — which collide with the target node's own prop types.
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 theme.accent', borderRadius: 6, backgroundColor: 'theme.warning', color: '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
ComponentHOC 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', }), ), })
The key is not optional dressing: without one React matches rows by position,
so deleting a row gives the survivors the previous row's state. MeoNode reports
a list that is missing them. For keys rows from the data instead — see
Lists and keys.
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
theme.colors.primary,theme.spacing.md - Global style changes through centralized theme configuration
- Consistent visual language across your entire application
Augment the MeoTheme interface to get autocomplete on every 'theme.<path>' string, and a typed theme wherever you receive one:
declare module '@meonode/ui' { interface MeoTheme { mode: 'light' | 'dark' system: { colors: { primary: string; bg: string } spacing: { sm: string; md: string } } } }
See the Theming guide for the full setup.
MeoNode reports problems at four different levels, and which one you are at decides what you see. Most of it needs no setup.
Always on in development
These fire whenever NODE_ENV is anything but production. No flag, no import — if you are running a dev server, you already have them.
Theme tokens
- a
theme.*token the active theme does not define, naming the property it was used for and thevar()it emitted — the usual cause of a style that silently does not apply, since the browser drops a declaration referencing an undefined variable - a number written as a string (
padding: '8'), which Emotion does not addpxto the way it does for a real number, so it emits an invalid length that is dropped - a length with no unit inside a media query (
'(min-width: 600)'), which is not a valid media feature. Media queries cannot read CSS variables, so a numeric theme token is substituted as bare text there and has to carry its own unit - a
csskey — an at-rule condition or a selector such as'@media (width >= var(--meonode-theme-breakpoint-wide))'— whose theme token could not be resolved, naming the key. A condition or selector needs the token's concrete value, so the rule is left out rather than written with the token in it; render the node under aThemeProviderwhose tokens define it, or write the value into the key - a theme function (
theme => …) with noThemeProviderabove it, naming the property it was written for when there is one. It is dropped rather than emitted, so without this line the style is simply missing
Lists
For()reporting, once per call site, that every item was a new object on a second render — reference identity cannot match those rows, so each one remounts. See Lists and keys- a
childrenarray that contains itself. The self-reference is dropped so the rest of the list still renders, rather than recursing until the stack gives out
Server-compiled styles
- a class handed to an element across a
'use cache'boundary that could not be combined with the element's owncss. The element keeps both classes, so stylesheet order decides a conflict between them; pass the styles ascssrather than as aclassNameto have them combined
Children
- an error thrown by a function-as-a-child. It is caught so one bad row cannot take the page down, and the child becomes
null— which means that without this line there is nothing to see at all: the element is simply not there, and a blank space does not suggest a flag to turn on
The function-as-a-child error and the self-referencing children array arrived in @meonode/ui 2.2.0. Before that, a function child's error was silent unless setDebugMode was on, and a children array could not contain arrays at all, so there was no self-reference to report.
Always, in every build
ThemeProvider throws when tokens, modes or defaultMode is missing, when defaultMode or defaultPreference names a mode that modes does not declare, or when defaultPreference is 'system' with no system mapping. The error names the prop. It is not a development check: a caller TypeScript never saw — plain JavaScript, a sandbox, a stale copy of an example — is most likely running a production bundle, and a check that goes quiet there helps nobody.
Behind setDebugMode(true)
import { setDebugMode } from '@meonode/ui' setDebugMode(true)
A process-wide switch, not per-component. Call it at module scope in your entry file, before anything renders — theme diagnostics fire as nodes are constructed, so enabling it from a useEffect misses the first render's worth.
It turns on everything above and these, which are quiet otherwise:
- a compiled marker whose
c/dbucket contains a reserved key that would shadow a top-level value - an error thrown while detecting whether a child is a function child — the probe that reads
prototype.render— which recovers by treating it as one - an error thrown while deriving a key name for a row a function child returned, which falls back to
item-<index>
What those three have in common is that MeoNode is inspecting itself, and the render is correct afterwards: there is nothing for you to act on, so they stay quiet unless you go looking. The development set above is about your code. Executing a function child sits in that set, not this one — the error is yours, and so is the element that goes missing.
It also forces the development diagnostics on in a production build, which is the main reason to reach for it: the flag is a normal runtime binding rather than a build-time constant, so a bundler cannot eliminate it. What a production build does fold away is the development half of the gate — NODE_ENV is read once, and a bundler that substitutes it settles that half at false — which is precisely why the flag still has something to turn on. The bugs worth this much logging are often the ones that only reproduce in production.
Only with @meonode/compiler
- React's own "Each child in a list should have a unique key" warning, on a list built by
.map()or any other generated expression - with
callSiteLocationsturned on, a line of ours naming where that list was written:[MeoNode] A generated list at src/app/page.ts:124:6 has children without a key.It needs no flag in development, and arrived in@meonode/compiler0.9.0 with@meonode/ui2.2.0
Neither is a flag, and the first is not ours. Whether a list was generated or written out by hand is decidable only from the source, so the compiler marks the call site and the runtime hands those children to React as a list for React to check. Without the compiler you get no report — and exactly the same positional-matching bug, since that is React's behaviour either way. For fixes it with or without a compiler.
The location line is there because React's warning cannot name the list. Every node in the tree is built by the same .render() call, so React's report points at that one line whichever list is missing keys. The compiler already knows where the generated expression was written, so it records the position and the runtime prints it alongside.
When it is worth turning debug on
- A style does not apply and the CSS looks right. Usually a
theme.*token resolving to nothing — but you already get that in dev without the flag, so reach forsetDebugModewhen it only reproduces in production. - You are using
@meonode/compilerand output looks off. The marker warning catches a malformed or stale compiled call site, and it is the only compiled-output warning behind the flag. The call-site location line is the other one, and development gives you that without asking. - A function child renders nothing. Development prints the thrown error with no setup; reach for the flag when it only happens in a production build.
Notes
Turn it off with setDebugMode(false). Logging goes to console at log, warn, and error, all prefixed [MeoNode] or MeoNode:, so it filters cleanly in devtools.