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 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
- Context-Based Theming: Always wrap your app with
ThemeProvider(usually via aWrappercomponent) to ensure theme context is available everywhere. - Portal System: Include
PortalProviderandPortalHostin your root wrapper. Portals opened viausePortalwill automatically inherit the application context (theme, redux, etc.). - Functional Components: Portal content should be regular functional components that return
.render()and acceptPortalLayerProps. - Tree Shaking: Vite's ES modules approach naturally supports tree shaking for MeoNode UI components.
- Persistence: Use
localStoragein yourWrapperto persist the user's theme preference.
Additional Resources
- Live Example: Check out the meonode-vite repository.
- Redux Toolkit: Official Documentation.
- Vite: Official Documentation.
Related FAQ
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
- Vite + React + TypeScript Integration Guide for MeoNode UI