• Getting Started
    • Overview
    • Why Without JSX?
    • Installation
    • Usage
    • Styling
    • Theming
    • Portal System
    • Rules & Patterns
    • Framework Integration
    • 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') }],
  },
})

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