Last active
January 21, 2024 22:08
-
-
Save jvaill/d1f7f2226361882dff01402dedb3dba5 to your computer and use it in GitHub Desktop.
A tiny file-based router for Vite!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Outlet } from "/tiny-vite-router"; | |
const NestedRoute = () => ( | |
<> | |
<h1>A nested route!</h1> | |
<Outlet /> | |
</> | |
); | |
export default NestedRoute; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Outlet } from "/tiny-vite-router"; | |
const NestestRoute = () => ( | |
<> | |
<h1>The most nested route!</h1> | |
<Outlet /> | |
</> | |
); | |
export default NestestRoute; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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