Skip to content

Instantly share code, notes, and snippets.

@jvaill
Last active January 21, 2024 22:08
Show Gist options
  • Save jvaill/d1f7f2226361882dff01402dedb3dba5 to your computer and use it in GitHub Desktop.
Save jvaill/d1f7f2226361882dff01402dedb3dba5 to your computer and use it in GitHub Desktop.
A tiny file-based router for Vite!
import React from "react";
import ReactDOM from "react-dom/client";
import { Outlet, TinyViteRouter } from "/tiny-vite-router.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<TinyViteRouter>
<Outlet />
</TinyViteRouter>
</React.StrictMode>
);
import { Outlet } from "/tiny-vite-router";
const RootRoute = () => (
<>
<h1>Root route</h1>
<ul>
<li>
<a href="/nested">Navigate to /nested</a>
</li>
<li>
<a href="/nested/nestest">Navigate to /nested/nestest</a>
</li>
</ul>
<div>
<Outlet />
</div>
</>
);
export default RootRoute;
import { Outlet } from "/tiny-vite-router";
const NestedRoute = () => (
<>
<h1>A nested route!</h1>
<Outlet />
</>
);
export default NestedRoute;
import { Outlet } from "/tiny-vite-router";
const NestestRoute = () => (
<>
<h1>The most nested route!</h1>
<Outlet />
</>
);
export default NestestRoute;
import { createContext, useContext, useEffect, useState } from "react";
import React from "react";
type Module = { default: object };
const isModule = (module: unknown): module is Module => {
return !!module && typeof module === "object" && "default" in module;
};
type RoutingTree = {
module?: Module;
segments?: Record<string, RoutingTree>;
};
const routingTree: RoutingTree = {};
const addRoute = (
module: Module,
segments: string[],
curTree: RoutingTree = routingTree
) => {
const [segment, ...newSegments] = segments;
if (!segment) {
curTree.module = module;
return;
}
curTree.segments ||= {};
curTree.segments[segment] ||= {};
addRoute(module, newSegments, curTree.segments[segment]);
};
const getRouteModulesForSegment = (
segments: string[],
path = segments.join("/"),
curTree: RoutingTree = routingTree
): Module[] => {
const module = curTree.module ? [curTree.module] : [];
const [segment, ...nextSegments] = segments;
if (!segment) {
return module;
}
const nextTree = curTree.segments?.[segment];
if (!nextTree) {
throw new Error(`Unknown route: /${path}`);
}
return [
...module,
...getRouteModulesForSegment(nextSegments, path, nextTree),
];
};
const getRouteModulesForPathname = (
pathname: string = window.location.pathname
) => {
const segments = pathname.replace(/^\/|\/$/g, "").split("/");
return getRouteModulesForSegment(segments);
};
const routeModules = import.meta.glob("./routes/**/*.tsx", { eager: true });
for (const [path, module] of Object.entries(routeModules)) {
const match = path.match(/^\.\/routes\/((?<segments>.*)\/)?index.tsx/);
if (!match) {
console.warn(`Skipping route with invalid filename: ${path}`);
continue;
}
if (!isModule(module)) {
console.warn(`Skipping route without default export: ${path}`);
continue;
}
const segments = match.groups?.segments?.split("/") ?? [];
addRoute(module, segments);
}
const RouterContext = createContext<{
modules: Module[];
} | null>(null);
export const TinyViteRouter: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [modules, setModules] = useState<Module[]>(getRouteModulesForPathname);
const handlePopstate = () => {
setModules(getRouteModulesForPathname());
};
useEffect(() => {
window.addEventListener("popstate", handlePopstate);
return () => {
window.removeEventListener("popstate", handlePopstate);
};
});
const handleWindowClick = (event: MouseEvent) => {
if (!(event.target instanceof HTMLElement)) {
return;
}
const anchor = event.target.closest("a");
if (anchor && anchor.origin === window.origin) {
event.preventDefault();
window.history.pushState(null, "", anchor.href);
window.dispatchEvent(new PopStateEvent("popstate"));
}
};
useEffect(() => {
window.addEventListener("click", handleWindowClick);
return () => {
window.removeEventListener("click", handleWindowClick);
};
});
return (
<RouterContext.Provider value={{ modules }}>
{children}
</RouterContext.Provider>
);
};
export const Outlet: React.FC = () => {
const routerContext = useContext(RouterContext);
const [module, ...modules] = routerContext!.modules;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ModuleDefaultExport = module?.default as any;
return (
<RouterContext.Provider value={{ modules }}>
{ModuleDefaultExport ? <ModuleDefaultExport /> : null}
</RouterContext.Provider>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment