UNPKG

vite-plugin-react-server

Version:
111 lines (103 loc) 3.6 kB
import { useEffect, useCallback } from "react"; import { RSC_HMR_EVENT } from "./createReactFetcher.js"; import type { RscHmrData } from "./createReactFetcher.js"; import { env } from "./env.js"; // Mirror of refreshCssLinks in virtualRscHmrPlugin.ts (see comment there). // data.file is the project-relative path (eg "src/css/9mmc.module.css") and // the link href is a URL whose pathname ends with the same suffix. // Falls back to basename matching for edge cases where the project-relative // form doesn't appear verbatim in the href. function refreshCssLinks(data: RscHmrData): boolean { if (!data || typeof document === "undefined") return false; const rel = String(data.file || "").replace(/^[\\/]+/, ""); if (!rel) return false; const basename = rel.split(/[\\/]/).pop(); const links = document.querySelectorAll('link[rel="stylesheet"]'); let refreshed = 0; links.forEach((link) => { const href = link.getAttribute("href"); if (!href) return; let pathname: string; try { pathname = new URL(href, window.location.origin).pathname; } catch { pathname = href.split("?")[0] ?? ""; } const matches = pathname.endsWith("/" + rel) || pathname.endsWith(rel) || (!!basename && pathname.endsWith("/" + basename)); if (!matches) return; const url = new URL(href, window.location.origin); url.searchParams.set("t", String(Date.now())); link.setAttribute("href", url.pathname + url.search); refreshed++; }); return refreshed > 0; } /** * React hook for RSC HMR (Hot Module Replacement). * * When a server component file changes, this hook calls your `refetch` function * to re-fetch the RSC stream. Combined with `startTransition`, this preserves * client component state while updating server-rendered content. * * @example * ```tsx * import { useRscHmr } from 'vite-plugin-react-server/utils'; * * function Shell({ data }) { * const [storeData, setStoreData] = useState(data); * * const refetch = useCallback((url: string) => { * startTransition(() => { * setStoreData(createReactFetcher({ url })); * }); * }, []); * * // Refetch RSC stream when server components change * useRscHmr(refetch); * * return <>{use(storeData)}</>; * } * ``` * * @param refetch - Function to call when server components change. * Receives the current pathname. Use `startTransition` inside for smooth updates. * @param options - Optional configuration */ export function useRscHmr( refetch: (url: string) => void, options: { /** Whether to log HMR events. @default true in dev */ verbose?: boolean; /** Custom filter — return false to skip refetch for specific files */ filter?: (data: RscHmrData) => boolean; } = {} ) { const { verbose = env.DEV, filter } = options; const handler = useCallback( (data: RscHmrData) => { if (filter && !filter(data)) return; const kind = (data as { kind?: string }).kind; if (verbose) { console.log('[RSC HMR] Server component updated:', data.file, kind ? `(${kind})` : ''); } if (kind === 'css') { refreshCssLinks(data); } refetch(window.location.pathname); }, [refetch, verbose, filter] ); useEffect(() => { if (typeof import.meta.hot === 'undefined') return; import.meta.hot.on(RSC_HMR_EVENT, handler); if (verbose) { console.log('[RSC HMR] Listening for server component updates'); } return () => { import.meta.hot!.off(RSC_HMR_EVENT, handler); }; }, [handler, verbose]); }