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') }], }, })
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
- 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