DEV Community

Akash for MechCloud Academy

Posted on • Edited on

2

Hot Reloading Vue Components from a Local UI Library Without Page Refresh

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
Enter fullscreen mode Exit fullscreen mode
  • Export Components: In src/index.js, export the components using relative paths to avoid alias conflicts:
  export { default as Button } from './components/Button.vue';
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • 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'),
      },
    },
  });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Alternatively, use a file: dependency in main-app/package.json:

"dependencies": {
  "my-ui-lib": "file:../path-to-my-ui-lib"
}
Enter fullscreen mode Exit fullscreen mode

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'],
  },
});
Enter fullscreen mode Exit fullscreen mode
  • resolve.alias: Points my-ui-lib to the library’s src folder, ensuring Vite loads raw .vue files instead of a built dist 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>
Enter fullscreen mode Exit fullscreen mode

Step 5: Run the Development Server

Start the main app’s Vite dev server:

cd path/to/main-app
pnpm run dev
Enter fullscreen mode Exit fullscreen mode

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’s src in main-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') },
    ],
  }
Enter fullscreen mode Exit fullscreen mode

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 in main-app/vite.config.js as shown above.
    • Run pnpm run build in main-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 into main-app/dist. No separate build is needed for my-ui-lib.
  • Option 2: Pre-build the Library:

    • Add a build script to my-ui-lib/vite.config.js to produce a dist 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 from main-app/vite.config.js to use the built version from node_modules.
    • Build the main app:
    cd path/to/main-app
    pnpm run build
    

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 in main-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.
  • Alias Conflicts:
    • If @ imports in my-ui-lib resolve incorrectly, switch to relative paths or use @lib as described.
  • pnpm Symlink Issues:
    • If pnpm link fails, try pnpm install with a file: dependency or clear the pnpm store (pnpm store prune).
  • 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.

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!

Gen AI apps are built with MongoDB Atlas

Gen AI apps are built with MongoDB Atlas

MongoDB Atlas is the developer-friendly database for building, scaling, and running gen AI & LLM apps—no separate vector DB needed. Enjoy native vector search, 115+ regions, and flexible document modeling. Build AI faster, all in one place.

Start Free

Top comments (0)

World's Largest Hackathon Awards Ceremony

Join us for the World’s Largest Hackathon Award Ceremony as we announce the winners and recognize the amazing work from this community!

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️