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
.vue
files 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-lib
to the library’ssrc
folder, ensuring Vite loads raw.vue
files instead of a builtdist
folder. -
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’ssrc
inmain-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.alias
inmain-app/vite.config.js
as shown above. - Run
pnpm run build
inmain-app
:
cd path/to/main-app pnpm run build
- Vite’s Rollup-based build compiles the library’s
.vue
files from../path-to-my-ui-lib/src
and 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.js
to produce adist
folder:
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-lib
alias frommain-app/vite.config.js
to 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.include
path inmain-app/vite.config.js
matches../path-to-my-ui-lib/src/**/*
. - Check the browser console for module resolution errors.
- Run
vite --debug
to log HMR events and confirm Vite is watching the library’s files.
- Verify the
-
Alias Conflicts:
- If
@
imports inmy-ui-lib
resolve incorrectly, switch to relative paths or use@lib
as described.
- If
-
pnpm Symlink Issues:
- If
pnpm link
fails, trypnpm install
with 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-app
build. - For Option 2, verify the library’s
dist
folder 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)