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

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 from '@vitejs/plugin-react-swc'
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'

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

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

Theme Configuration

Theme Objects

Create theme objects structure with mode and system properties:

import { Theme } from '@meonode/ui'

const lightTheme: Theme = {
  mode: 'light',
  system: {
    primary: {
      default: '#3B82F6',
      content: '#FFFFFF',
    },
    secondary: {
      default: '#6B7280',
      content: '#FFFFFF',
    },
    accent: {
      default: '#F59E0B',
      content: '#000000',
    },
    base: {
      default: '#FFFFFF',
      content: '#111827',
    },
    surface: {
      default: '#F9FAFB',
      content: '#374151',
    },
    success: {
      default: '#10B981',
      content: '#FFFFFF',
    },
    warning: {
      default: '#F59E0B',
      content: '#000000',
    },
    error: {
      default: '#EF4444',
      content: '#FFFFFF',
    },
    spacing: {
      xs: '4px',
      sm: '8px',
      md: '16px',
      lg: '24px',
      xl: '32px',
      '2xl': '48px',
    },
    text: {
      xs: '0.75rem',
      sm: '0.875rem',
      base: '1rem',
      lg: '1.125rem',
      xl: '1.25rem',
      '2xl': '1.5rem',
      '3xl': '1.875rem',
      '4xl': '2.25rem',
    },
  },
}

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, TypedUseSelectorHook } from 'react-redux'

export const store = configureStore({
  reducer: {
    // Add your app-specific reducers here
  },
})

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

export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

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 NodeElement, type Theme, 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'
import { ThemeProvider as MeoThemeWrapper } from '@meonode/ui'

interface WrappersProps {
  children: NodeElement
}

const ThemeWrapper = ({ children }: { children?: Children }) => {
  const initialTheme = useMemo<Theme>(() => {
    const stored = localStorage.getItem('theme')
    return stored === 'dark' ? darkTheme : lightTheme
  }, [])

  return MeoThemeWrapper({ theme: initialTheme, children }).render()
}

export const Wrapper = ({ children }: WrappersProps) =>
  Node(ReduxProvider, {
    store,
    children: Node(ThemeWrapper, {
      children: Node(SnackbarProvider, {
        children: PortalProvider({
          children: [children, PortalHost()],
        }),
      }),
    }),
  })

Router Setup

Route Component

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

const App = lazy(() => import('@src/pages/App'))

const routes: RouteObject[] = [
  {
    path: '/',
    element: Node(Suspense, { 
      fallback: Center({ height: '100vh', children: Text('Loading...') }).render(), 
      children: Node(App) 
    }).render(),
  },
]

const router = createBrowserRouter(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 portal = usePortal()

  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 = ({ close }: PortalLayerProps) => {
  const [counter, setCounter] = useState(0)

  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: ${counter}`),
        Row({
          gap: 'theme.spacing.md',
          children: [
            Button('-', { onClick: () => setCounter(c => c - 1) }),
            Button('+', { onClick: () => 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: 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

On this page