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

Vite + React + TypeScript Integration Guide for MeoNode UI

Overview

@meonode/ui is a modern, type-safe React UI library that works perfectly with Vite + React + TypeScript projects. This guide provides a comprehensive setup for integrating MeoNode UI into a Vite-based React application, featuring the new Context-based theming system and the Portal system.


Vite Vite Integration

Installation & Setup

Create a new Vite project with React and TypeScript support:

# Create a new Vite app with React and TypeScript
npm create vite@latest my-app -- --template react-ts

# Navigate to your project directory
cd my-app

# Install the core MeoNode UI library
npm install @meonode/ui

# Install additional dependencies
npm install @reduxjs/toolkit react-redux

Vite Configuration

Update your Vite configuration to support Emotion (MeoNode UI's CSS engine):

import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import { visualizer } from 'rollup-plugin-visualizer'
import * as path from 'node:path'
import { dependencies } from './package.json'
import { imagetools } from 'vite-imagetools'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import babel from '@rolldown/plugin-babel'

function renderChunks(deps: Record<string, string>) {
  const chunks: Record<string, string[]> = { react: ['react'] }
  Object.keys(deps).forEach(key => {
    if (['react', 'react-dom'].includes(key)) return
    chunks[key] = [key]
  })
  return chunks
}

function manualChunks(id: string) {
  const chunks = renderChunks(dependencies)
  for (const [name, modules] of Object.entries(chunks)) {
    if (modules.some(m => id.includes(`/node_modules/${m}/`))) return name
  }
}

// https://vite.dev/config/
export default defineConfig({
  mode: process.env.NODE_ENV,
  plugins: [
    react(),
    babel({ presets: [reactCompilerPreset()] }),
    imagetools(),
    ViteImageOptimizer(),
    visualizer({ open: true, sourcemap: true, filename: 'bundle_report.html' }),
  ],
  build: {
    minify: 'esbuild',
    cssMinify: 'esbuild',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks,
      },
    },
  },
  resolve: {
    alias: [{ find: '@src', replacement: path.resolve(__dirname, 'src') }],
  },
})

Optional: build-time compiler

@meonode/compiler is an SWC plugin that moves prop classification, stable-key hashing and theme.* resolution out of every render and into your build. It is optional and safe to add or remove at any time.

npm install --save-dev @meonode/compiler
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

export default defineConfig({
  plugins: [react({ plugins: [['@meonode/compiler', {}]] })],
})

Roughly 1.6x off client renders. See the compiler guide for what compiles, what is left untouched, and how to verify the plugin actually ran.

Theme Configuration

Theme Objects

Create theme objects structure with mode and system properties:

import baseThemeSystem from './baseThemeSystem'
import type { MeoTheme } from '@meonode/ui'

const lightTheme: MeoTheme = {
  mode: 'light',
  system: {
    ...baseThemeSystem,
    primary: {
      default: '#000000',
      content: '#ffffff',
      muted: '#bdbdbd',
      hover: '#222222',
    },
    secondary: {
      default: '#370900',
      content: '#fed7a7',
      muted: '#a67c6b',
      hover: '#5a1400',
    },
    accent: {
      default: '#8c3f27',
      content: '#fed7a7',
      muted: '#b97a5a',
      hover: '#a44a2e',
    },
    neutral: {
      default: '#c93400',
      content: '#fff7ed',
      muted: '#e0a899',
      hover: '#e04a1a',
    },
    base: {
      default: '#fff7ed',
      medium: '#feebd3',
      deep: '#fed7a7',
      content: '#7a3205',
      muted: '#f5e1d3',
      hover: '#ffe7c2',
    },

    success: {
      default: '#2E7D32',
      content: '#fef3c5',
      muted: '#81c784',
      hover: '#388e3c',
    },
    warning: {
      default: '#F9A825',
      content: '#fffde7',
      muted: '#ffe082',
      hover: '#ffb300',
    },
    error: {
      default: '#D32F2F',
      content: '#ffebee',
      muted: '#e57373',
      hover: '#e53935',
    },
    danger: {
      default: '#C62828',
      content: '#ffcdd2',
      muted: '#ef9a9a',
      hover: '#d32f2f',
    },
    info: {
      default: '#0288D1',
      content: '#e1f5fe',
      muted: '#81d4fa',
      hover: '#039be5',
    },

    shadow: {
      default: '0 4px 12px rgba(0, 0, 0, 0.1)',
      sm: '0 1px 2px rgba(0, 0, 0, 0.05)',
      md: '0 2px 6px rgba(0, 0, 0, 0.08)',
      lg: '0 8px 16px rgba(0, 0, 0, 0.12)',
      xl: '0 12px 24px rgba(0, 0, 0, 0.15)',
      '2xl': '0 16px 32px rgba(0, 0, 0, 0.2)',
    },
  },
}

export default lightTheme

Redux Toolkit Integration (Optional)

Since MeoNode UI v0.3+ provides built-in theme management through Context, you only need Redux for your application's business logic, not for theme management.

Store Configuration (Simplified)

import { configureStore } from '@reduxjs/toolkit'
import { useDispatch, useSelector } from 'react-redux'
import appSlice from '@src/redux/slice/app.slice.ts'

export const store = configureStore({
  reducer: {
    appReducer: appSlice,
    // Add your reducers here
  },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()

Provider Setup

Simplified Providers Component

Create a provider component using MeoNode UI's built-in theme and portal management:

import { useMemo } from 'react'
import { store } from '@src/redux/store'
import { type Children, Node, type Theme, ThemeProvider, PortalProvider, PortalHost } from '@meonode/ui'
import { Provider as ReduxProvider } from 'react-redux'
import { SnackbarProvider } from 'notistack'
import lightTheme from '@src/constants/themes/lightTheme.ts'
import darkTheme from '@src/constants/themes/darkTheme.ts'

interface WrappersProps {
  children: Children
}

export const Wrapper = ({ children }: WrappersProps) => {
  const theme = useMemo<Theme>(() => {
    // Initialize from localStorage
    const stored = localStorage.getItem('theme')
    return stored === 'dark' ? darkTheme : lightTheme
  }, [])

  return Node(ReduxProvider, {
    store,
    children: ThemeProvider({
      theme,
      children: Node(SnackbarProvider, {
        children: PortalProvider({
          children: Array.isArray(children) ? [...children, PortalHost()] : [children, PortalHost()],
        }),
      }),
    }),
  }).render()
}

Router Setup

Route Component

import { createBrowserRouter, type RouteObject, RouterProvider } from 'react-router'
import { lazy, Suspense } from 'react'
import { Absolute, Center, Node, type NodeElementType } from '@meonode/ui'
import { CircularProgress } from '@meonode/mui'

type RouteType = Omit<RouteObject, 'children' | 'element'> & {
  element?: NodeElementType
  children?: RouteType[]
}

const routes: RouteType[] = [
  {
    path: '/',
    element: lazy(() => import('@src/pages/App.tsx')),
  },
]

const Fallback = Absolute({
  top: 0,
  left: 0,
  right: 0,
  bottom: 0,
  backgroundColor: 'theme.base',
  children: Center({ height: '100%', children: CircularProgress({ size: 50, color: 'theme.base.content' }) }),
}).render()

const wrapElement = (routes: RouteType[]): RouteObject[] => {
  return routes.map(route => ({
    ...route,
    element: route.element ? Node(Suspense, { fallback: Fallback, children: Node(route.element) }).render() : undefined,
    children: route.children ? wrapElement(route.children) : undefined,
  })) as RouteObject[]
}

const router = createBrowserRouter(wrapElement(routes))
const Routes = () => Node(RouterProvider, { router })
export default Routes

Application Components

App Component

import { Center, Column, Row, H1, H2, Button, Text, Div, usePortal, useTheme, type PortalLayerProps } from '@meonode/ui'
import { useState } from 'react'
import darkTheme from '@src/constants/themes/darkTheme.ts'
import lightTheme from '@src/constants/themes/lightTheme.ts'

const App = () => {
  const { setTheme, mode } = useTheme()
  const [counter, setCounter] = useState(0)
  const portal = usePortal({ counter, setCounter })

  return Center({
    minHeight: '100dvh',
    backgroundColor: 'theme.base',
    color: 'theme.base.content',
    children: Column({
      gap: 'theme.spacing.lg',
      textAlign: 'center',
      children: [
        H1('Vite + MeoNode UI', { fontSize: 'theme.text.4xl' }),
        
        Row({
          gap: 'theme.spacing.md',
          children: [
            Button('🎯 Open Demo Portal', {
              onClick: () => portal.open(InteractiveDemo)
            }),
            Button('Toggle Theme', {
              onClick: () => setTheme(t => t.mode === 'light' ? darkTheme : lightTheme)
            }),
          ]
        })
      ],
    }),
  }).render()
}

// Interactive demo modal component
const InteractiveDemo = ({ data, close }: PortalLayerProps) => {
  return Center({
    position: 'fixed',
    inset: 0,
    backgroundColor: 'rgba(0,0,0,0.6)',
    backdropFilter: 'blur(8px)',
    zIndex: 1000,
    onClick: e => e.target === e.currentTarget && close(),
    children: Column({
      backgroundColor: 'theme.base',
      padding: 'theme.spacing.2xl',
      borderRadius: '20px',
      gap: 'theme.spacing.lg',
      children: [
        H2('Interactive Portal'),
        Text(`Counter: ${data.counter}`),
        Row({
          gap: 'theme.spacing.md',
          children: [
            Button('-', { onClick: () => data.setCounter(c => c - 1) }),
            Button('+', { onClick: () => data.setCounter(c => c + 1) }),
          ]
        }),
        Button('Close', { onClick: close })
      ]
    })
  }).render()
}

export default App

Main Entry Point

Updated main.tsx

import { StrictMode } from 'react'
import { Node } from '@meonode/ui'
import Routes from '@src/routes'
import { Wrapper } from '@src/components/Wrapper.ts'
import '@src/assets/global.css'
import { render } from '@meonode/ui/client'

const App = Node(StrictMode, { children: Node(Wrapper, { children: Routes() }) })
render(App, document.getElementById('root')!)

Best Practices for Vite Integration

  1. Context-Based Theming: Always wrap your app with ThemeProvider (usually via a Wrapper component) to ensure theme context is available everywhere.
  2. Portal System: Include PortalProvider and PortalHost in your root wrapper. Portals opened via usePortal will automatically inherit the application context (theme, redux, etc.).
  3. Functional Components: Portal content should be regular functional components that return .render() and accept PortalLayerProps.
  4. Tree Shaking: Vite's ES modules approach naturally supports tree shaking for MeoNode UI components.
  5. Persistence: Use localStorage in your Wrapper to persist the user's theme preference.

Additional Resources


Why is Hot Module Replacement (HMR) not working for my sub-components?

In Vite, ensure sub-components follow stable component boundaries and expected file patterns. Hook-using components should be wrapped correctly to avoid stale updates.

How do I use existing JSX components or JSX libraries with MeoNode UI?

Use Node() wrappers (or package-level wrappers like @meonode/mui) so JSX-based libraries remain compatible with your MeoNode composition style.

More details: /docs/getting-started/faq

On this page