Files
multica/packages/views/navigation/context.tsx
Naiyuan Qing e8d6c912c4 feat(views): prefetch + transition + skeleton for snappy web navigation (MUL-2269) (#2677)
Internal navigation on web feels laggy because clicking a sidebar link blocks
0.2–0.6s with zero visual feedback — no prefetch, no Suspense fallback in the
dashboard segment, and no React transition to mark the route commit as pending.

This change adds the three pieces App Router needs to make the click→commit
window feel instant, scoped to the (dashboard) segment so auth/landing keep
their existing chrome:

- NavigationAdapter gains an optional prefetch(path). The web adapter wires
  it to router.prefetch; desktop leaves it undefined (react-router has no
  equivalent and doesn't need one). AppLink prefetches on hover/focus and
  preserves caller-supplied onMouseEnter/onFocus/onClick.
- NavigationProvider wraps push/replace in useTransition and exposes the
  pending flag via useIsNavigating(). Every useNavigation().push caller —
  sidebar AppLink, command palette, post-create modal jumps — picks this up
  automatically.
- New apps/web/app/[workspaceSlug]/(dashboard)/loading.tsx renders a minimal
  skeleton during cold transitions inside the dashboard segment only.
- DashboardLayout renders a 1px top progress bar driven by useIsNavigating.

packages/views remains free of next/* imports; desktop is unaffected by
construction (no prefetch, transition flips quickly, no loading.tsx).

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 17:01:42 +08:00

50 lines
1.6 KiB
TypeScript

"use client";
import { createContext, useContext, useMemo, useTransition } from "react";
import type { NavigationAdapter } from "./types";
const NavigationContext = createContext<NavigationAdapter | null>(null);
const NavigationPendingContext = createContext<boolean>(false);
export function NavigationProvider({
value,
children,
}: {
value: NavigationAdapter;
children: React.ReactNode;
}) {
// Wrap push/replace in startTransition so any caller of useNavigation()
// (sidebar AppLink, command palette, modal post-create jumps) gets a
// React pending signal during route commit. On web this stays true until
// Next.js commits the new RSC payload; on desktop it flips off quickly
// because react-router commits synchronously — both are correct.
const [isPending, startTransition] = useTransition();
const wrapped = useMemo<NavigationAdapter>(
() => ({
...value,
push: (path: string) => startTransition(() => value.push(path)),
replace: (path: string) => startTransition(() => value.replace(path)),
}),
[value],
);
return (
<NavigationContext.Provider value={wrapped}>
<NavigationPendingContext.Provider value={isPending}>
{children}
</NavigationPendingContext.Provider>
</NavigationContext.Provider>
);
}
export function useNavigation(): NavigationAdapter {
const ctx = useContext(NavigationContext);
if (!ctx)
throw new Error("useNavigation must be used within NavigationProvider");
return ctx;
}
/** True while a transition-wrapped push/replace is committing. */
export function useIsNavigating(): boolean {
return useContext(NavigationPendingContext);
}