npm.io
0.6.6 • Published 1h ago

@real-router/preload-plugin

Licence
MIT
Version
0.6.6
Deps
2
Size
55 kB
Vulns
0
Weekly
0
Stars
6

@real-router/preload-plugin

npm npm downloads bundle size License: MIT

Preload on navigation intent for Real-Router. Trigger data preloading when users hover over or touch links — before they click.

// Without plugin — data loads AFTER navigation:
// click → navigate → render → fetch → re-render (waterfall)

// With plugin — data loads BEFORE navigation:
// hover → preload → click → navigate → render (data ready)

Installation

npm install @real-router/preload-plugin

Peer dependency: @real-router/core

Runtime dependency: @real-router/browser-plugin must be registered (provides matchUrl for URL → route resolution).

Quick Start

import { createRouter } from "@real-router/core";
import { browserPluginFactory } from "@real-router/browser-plugin";
import { preloadPluginFactory } from "@real-router/preload-plugin";

const routes = [
  {
    name: "users.profile",
    path: "/users/:id",
    preload: async (params) => {
      await queryClient.prefetchQuery({
        queryKey: ["user", params.id],
        queryFn: () => fetchUser(params.id),
      });
    },
  },
  {
    name: "products.detail",
    path: "/products/:slug",
    preload: async (params) => {
      await productStore.prefetch(params.slug);
    },
  },
];

const router = createRouter(routes);
router.usePlugin(browserPluginFactory(), preloadPluginFactory());

await router.start();

When a user hovers over a <Link routeName="users.profile" routeParams={{ id: '123' }}> for 65ms, the plugin calls preload({ id: '123' }) — warming up your data layer before navigation.

Options

router.usePlugin(
  preloadPluginFactory({
    delay: 100, // Hover debounce in ms (default: 65)
    networkAware: true, // Disable on Save-Data / 2G (default: true)
  }),
);
Option Type Default Description
delay number 65 Milliseconds to wait before triggering hover preload
networkAware boolean true Skip preloading on Save-Data or 2G connections

How It Works

Zero adapter changes

The plugin uses DOM-level event delegation — listeners on document, not on individual <Link> components. No modifications to React, Vue, Preact, Solid, or Svelte adapters.

Intent detection
Trigger Event Timing Rationale
Hover mouseover Debounced (configurable) Filter accidental mouse passes
Touch touchstart ~100ms (scroll detection) Touch = strong intent signal
Route resolution
anchor.href → router.matchUrl(href) → State → getRouteConfig(state.name)?.preload → call

External links, routes without preload, and non-matching URLs are silently skipped.

Mobile support
  • Touch preloading: touchstart triggers preload with minimal delay
  • Scroll detection: touchmove with >10px vertical movement cancels pending preload
  • Ghost event suppression: Synthetic mouseover events fired by mobile browsers after touchstart are suppressed (prevents double-preload)

All listeners use { passive: true } — never blocks scrolling.

Network awareness

Preloading is automatically disabled when:

  • navigator.connection.saveData is enabled
  • navigator.connection.effectiveType is 2g or slow-2g

Disable with networkAware: false if your preload functions handle this themselves.

<!-- Disable preloading for a specific link -->
<a href="/heavy-page" data-no-preload>Heavy Page</a>

Works in all frameworks via prop pass-through:

<Link routeName="heavy" data-no-preload>
  Heavy Page
</Link>

Router Extension

The plugin adds one method to the router:

router.getPreloadSettings();
// → { delay: 65, networkAware: true }

Data Layer Integration

The plugin is data-agnostic — it calls your preload function and doesn't care about the result. You control what happens inside:

TanStack Query
{
  name: "users.profile",
  path: "/users/:id",
  preload: async (params) => {
    await queryClient.prefetchQuery({
      queryKey: ["user", params.id],
      queryFn: () => fetchUser(params.id),
    });
  },
}
Zustand / Pinia / Custom Store
{
  name: "products.detail",
  path: "/products/:slug",
  preload: async (params) => {
    await productStore.prefetch(params.slug);
  },
}
Multiple Concerns
{
  name: "dashboard",
  path: "/dashboard",
  preload: async () => {
    await Promise.all([
      queryClient.prefetchQuery({ queryKey: ["stats"], queryFn: fetchStats }),
      queryClient.prefetchQuery({ queryKey: ["recent"], queryFn: fetchRecent }),
    ]);
  },
}

Errors in preload are silently caught — error handling is your data layer's responsibility.

SSR Support

The plugin is SSR-safe — returns an empty plugin object when document is not available:

// Server-side — no errors, no listeners
router.usePlugin(preloadPluginFactory());

Graceful Degradation

Without browser-plugin, router.matchUrl is undefined. The plugin silently skips preloading via optional chaining — no errors, no warnings.

Documentation

Package Description
@real-router/core Core router (required peer dependency)
@real-router/browser-plugin Browser plugin (provides matchUrl)
@real-router/lifecycle-plugin Route-level lifecycle hooks

Contributing

See contributing guidelines for development setup and PR process.

License

MIT Oleg Ivanov

Keywords