The Problem: Page Refresh When Updating a Local UI Components Library
Building a local UI components library for a Vue.js application is a powerful way to modularize and reuse code across projects. By maintaining a separate library (e.g., my-ui-lib) with Vue single-file components (SFCs), developers can share consistent UI elements like buttons, modals, or form inputs. However, during development, a common frustration arises: updating a component in the local UI library often triggers a full page refresh in the main application’s browser. This disrupts the development experience, slows iteration, and breaks the state of the application, making it harder to test changes in real-time.
The root cause lies in how the main Vue app and the local UI library are integrated. When using tools like Vite (a popular build tool for Vue.js), the default setup may not properly watch the library’s source files for changes, or it may rely on a pre-built library output (e.g., a dist folder) that doesn’t support hot module replacement (HMR). For example, if the library is built with vite build --watch (which uses Rollup), changes may trigger a full module reload rather than a targeted HMR update. This is especially problematic when the library is linked locally (e.g., via pnpm link) and the main app’s Vite dev server fails to detect source changes in the library.
To address this, we need a solution that enables true HMR, allowing changes to the UI library’s components to reflect in the browser without a full page refresh. This post walks through a robust setup using Vite, pnpm, and Vue.js to achieve seamless hot reloading for a local UI components library.
The Solution: Enabling HMR for a Local UI Library
To fix the page refresh issue and enable HMR, we need to configure the main Vue app’s Vite dev server to:
- Resolve the UI library’s source files directly, bypassing any pre-built output.
- Watch the library’s source files for changes.
- Compile the library’s
.vuefiles on the fly using Vite’s esbuild-based dev server.
We’ll use pnpm to link the library and handle alias conflicts (e.g., the @ alias commonly used for src folders). The following steps assume a setup with a main Vue app (main-app) and a local UI library (my-ui-lib), both using Vite and Vue 3.
Step 1: Set Up the UI Components Library
First, structure the UI library (my-ui-lib) as a Node module with Vue components. Here’s an example structure:
my-ui-lib/
├── src/
│ ├── components/
│ │ └── Button.vue
│ └── index.js
├── package.json
└── vite.config.js
-
Export Components: In
src/index.js, export the components using relative paths to avoid alias conflicts:
export { default as Button } from './components/Button.vue';
Using relative paths (e.g., ./components/Button.vue) prevents issues where the @ alias (commonly used for src) resolves to the main app’s src folder instead of the library’s.
- Configure package.json: Define the module entry and peer dependencies:
{
"name": "my-ui-lib",
"version": "1.0.0",
"main": "./src/index.js",
"module": "./src/index.js",
"exports": {
".": {
"import": "./src/index.js"
}
},
"peerDependencies": {
"vue": "^3.0.0"
}
}
-
Optional Vite Config: If you plan to develop or build the library standalone, add a
vite.config.js:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
});
Step 2: Link the Library with pnpm
Link the UI library to the main app using pnpm to enable local development:
cd path/to/my-ui-lib
pnpm link --global
cd path/to/main-app
pnpm link --global my-ui-lib
Alternatively, use a file: dependency in main-app/package.json:
"dependencies": {
"my-ui-lib": "file:../path-to-my-ui-lib"
}
Run pnpm install in main-app to resolve the dependency. This creates a symlink in main-app/node_modules/my-ui-lib pointing to the library’s folder.
Step 3: Configure Vite in the Main App for HMR
In the main app’s vite.config.js, configure Vite to resolve the UI library’s source files and watch them for changes. This ensures Vite’s esbuild-based dev server compiles the library’s .vue files on the fly and enables HMR.
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: [
// Main app's src alias
{ find: '@', replacement: path.resolve(__dirname, 'src') },
// UI library's src alias
{ find: 'my-ui-lib', replacement: path.resolve(__dirname, '../path-to-my-ui-lib/src') },
],
},
server: {
watch: {
// Watch the library's source files
include: [path.resolve(__dirname, '../path-to-my-ui-lib/src/**/*')],
},
},
optimizeDeps: {
include: ['my-ui-lib'],
},
});
-
resolve.alias: Points
my-ui-libto the library’ssrcfolder, ensuring Vite loads raw.vuefiles instead of a builtdistfolder. -
server.watch.include: Instructs Vite to monitor the library’s source files for changes, triggering HMR when a file (e.g.,
Button.vue) is modified. - optimizeDeps.include: Ensures esbuild pre-bundles the library’s dependencies for faster HMR.
Step 4: Use the Library Components in the Main App
In the main app, import and use the UI library’s components:
<template>
<Button label="Click Me" />
</template>
<script>
import { Button } from 'my-ui-lib';
export default {
components: { Button },
};
</script>
Step 5: Run the Development Server
Start the main app’s Vite dev server:
cd path/to/main-app
pnpm run dev
Now, when you edit my-ui-lib/src/components/Button.vue, Vite detects the change, recompiles only the affected component, and updates the browser without a full page refresh. This preserves the app’s state and provides a smooth development experience.
Step 6: Handling Alias Conflicts
If the UI library uses an @ alias for its src folder (e.g., import Something from '@/utils/helper.js'), it may resolve to the main app’s src folder due to Vite’s alias precedence. To fix this:
-
Preferred Approach: Use Relative Paths: Update the library to use relative paths (e.g.,
./components/Button.vue) instead of@. This avoids conflicts entirely. -
Alternative: Unique Alias: Define a unique alias (e.g.,
@lib) for the library’ssrcinmain-app/vite.config.js:
resolve: {
alias: [
{ find: '@', replacement: path.resolve(__dirname, 'src') },
{ find: 'my-ui-lib', replacement: path.resolve(__dirname, '../path-to-my-ui-lib/src') },
{ find: '@lib', replacement: path.resolve(__dirname, '../path-to-my-ui-lib/src') },
],
}
Then, update my-ui-lib imports to use @lib (e.g., import Something from '@lib/utils/helper.js').
Step 7: Packaging for Production
For production, you have two options to package the UI library and main app:
-
Option 1: Bundle Library Source (Recommended for Simplicity):
- Keep the
resolve.aliasinmain-app/vite.config.jsas shown above. - Run
pnpm run buildinmain-app:
cd path/to/main-app pnpm run build- Vite’s Rollup-based build compiles the library’s
.vuefiles from../path-to-my-ui-lib/srcand bundles them intomain-app/dist. No separate build is needed formy-ui-lib.
- Keep the
-
Option 2: Pre-build the Library:
- Add a build script to
my-ui-lib/vite.config.jsto produce adistfolder:
import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import path from 'path'; export default defineConfig({ plugins: [vue()], build: { lib: { entry: path.resolve(__dirname, 'src/index.js'), name: 'MyUiLib', fileName: (format) => `my-ui-lib.${format}.js`, }, rollupOptions: { external: ['vue'], output: { globals: { vue: 'Vue' }, }, }, }, resolve: { alias: { '@': path.resolve(__dirname, 'src') }, }, });- Update
my-ui-lib/package.json:
{ "main": "./dist/my-ui-lib.cjs.js", "module": "./dist/my-ui-lib.esm.js", "scripts": { "build": "vite build" } }- Build the library first:
cd path/to/my-ui-lib pnpm run build- Remove the
my-ui-libalias frommain-app/vite.config.jsto use the built version fromnode_modules. - Build the main app:
cd path/to/main-app pnpm run build - Add a build script to
For most cases, Option 1 is simpler, as it aligns with the development setup and requires only one build step. Use Option 2 if you need a reusable, distributable library (e.g., for npm publication).
Troubleshooting Common Issues
-
HMR Not Triggering:
- Verify the
server.watch.includepath inmain-app/vite.config.jsmatches../path-to-my-ui-lib/src/**/*. - Check the browser console for module resolution errors.
- Run
vite --debugto log HMR events and confirm Vite is watching the library’s files.
- Verify the
-
Alias Conflicts:
- If
@imports inmy-ui-libresolve incorrectly, switch to relative paths or use@libas described.
- If
-
pnpm Symlink Issues:
- If
pnpm linkfails, trypnpm installwith afile:dependency or clear the pnpm store (pnpm store prune).
- If
-
Build Errors:
- For Option 1, ensure the library’s source path is accessible during
main-appbuild. - For Option 2, verify the library’s
distfolder is generated and correctly linked.
- For Option 1, ensure the library’s source path is accessible during
Why This Works
Vite’s dev server leverages esbuild for fast, on-the-fly compilation of Vue SFCs, making it ideal for HMR. By aliasing my-ui-lib to its src folder and watching its files, Vite treats the library’s components as part of the main app’s module graph. When a component like Button.vue changes, Vite recompiles only that module and updates the browser via HMR, preserving the app’s state. Avoiding pre-built library outputs during development ensures no full module reloads occur, unlike Rollup-based watch modes (e.g., vite build --watch).
Conclusion
Building a local UI components library shouldn’t slow down your Vue.js development with constant page refreshes. By configuring Vite to resolve and watch the library’s source files, using pnpm for linking, and handling alias conflicts, you can achieve seamless HMR. Changes to your UI components reflect instantly in the browser, boosting productivity and maintaining a smooth developer experience. For production, bundling the library’s source with the main app simplifies the process, but pre-building the library offers flexibility for reuse.
Try this setup in your next Vue project, and enjoy a refresh-free development workflow with your local UI library!
Top comments (0)