Last active
December 21, 2025 08:07
-
-
Save kinngh/24f06116c671fe1bed0280092550acde to your computer and use it in GitHub Desktop.
File based routing with `preact` and `preact-iso`
This file contains hidden or 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
| //I prefer renaming it to src/entry-client.jsx | |
| //src/Index.jsx | |
| import { render } from "preact"; | |
| import { LocationProvider } from "preact-iso"; | |
| import RouterView from "./routing/RouterView"; | |
| export function App() { | |
| return ( | |
| <LocationProvider> | |
| <RouterView /> | |
| </LocationProvider> | |
| ); | |
| } | |
| const mount = document.getElementById("root") || document.getElementById("app"); | |
| if (!mount) { | |
| throw new Error("Missing mount element: expected #root or #app"); | |
| } | |
| render(<App />, mount); |
This file contains hidden or 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
| { | |
| "compilerOptions": { | |
| "paths": { | |
| "@router": ["./src/routing/router.jsx"], | |
| "@router/*": ["./src/routing/*"] | |
| } | |
| }, | |
| } |
This file contains hidden or 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
| //src/routing/router.jsx | |
| import { useLocation, useRoute } from "preact-iso"; | |
| /** | |
| * @typedef {Object} RouterLike | |
| * @property {(url: string, replace?: boolean) => void} push | |
| * @property {Record<string, string>} query | |
| * @property {Record<string, string>} params | |
| * @property {string} currentPath | |
| * @property {string} url | |
| */ | |
| /** | |
| * Router hook backed by preact-iso. | |
| * | |
| * @returns {RouterLike} | |
| */ | |
| export function useRouter() { | |
| const location = useLocation(); | |
| const route = useRoute(); | |
| return { | |
| push: (url, replace) => location.route(url, replace), | |
| query: route.query, | |
| params: route.params, | |
| currentPath: route.path, | |
| url: location.url, | |
| }; | |
| } | |
| export default useRouter; |
This file contains hidden or 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
| //src/routing/RouterView.jsx | |
| /// <reference types="vite/client" /> | |
| import { ErrorBoundary, Router, Route, lazy } from "preact-iso"; | |
| /** | |
| * @typedef {import('preact').ComponentType<any>} AnyComponent | |
| */ | |
| /** | |
| * A Vite dynamic import loader for a page module. | |
| * | |
| * @typedef {() => Promise<{ default: AnyComponent } & Record<string, any>>} Loader | |
| */ | |
| /** @type {Record<string, Loader>} */ | |
| const pageImports = import.meta.glob("../pages/**/!(_)*.{tsx,jsx}"); | |
| /** | |
| * @param {string} key | |
| * @returns {string} | |
| */ | |
| function pathToIsoPattern(key) { | |
| let routePath = key.replace(/^\.\.\/pages/, "").replace(/\.(t|j)sx$/, ""); | |
| if (routePath.endsWith("/index")) { | |
| routePath = routePath.replace(/\/index$/, "") || "/"; | |
| } | |
| routePath = routePath.replace(/\[\.\.\.([^/\]]+)\]/g, ":$1*"); | |
| routePath = routePath.replace(/\[([^/\]]+)\]/g, ":$1"); | |
| return routePath; | |
| } | |
| /** | |
| * @param {unknown} loader | |
| * @returns {() => Promise<{ default: AnyComponent }>} | |
| */ | |
| function toLazyModule(loader) { | |
| /** @type {Loader} */ | |
| const typed = loader; | |
| return async () => { | |
| const mod = await typed(); | |
| return { default: mod.default }; | |
| }; | |
| } | |
| function RouterView() { | |
| const entries = Object.entries(pageImports).filter( | |
| ([key]) => !key.includes("/pages/api/") | |
| ); | |
| const notFoundEntry = entries.find(([key]) => /\/404\.(t|j)sx$/.test(key)); | |
| const routes = entries | |
| .filter(([key]) => !/\/404\.(t|j)sx$/.test(key)) | |
| .map(([key, loader]) => { | |
| const path = pathToIsoPattern(key); | |
| const Component = lazy(toLazyModule(loader)); | |
| return <Route key={key} path={path} component={Component} />; | |
| }); | |
| const NotFound = notFoundEntry | |
| ? lazy(toLazyModule(notFoundEntry[1])) | |
| : function NotFoundFallback() { | |
| return <p>no route found</p>; | |
| }; | |
| return ( | |
| <ErrorBoundary> | |
| <Router> | |
| {routes} | |
| <Route default component={NotFound} /> | |
| </Router> | |
| </ErrorBoundary> | |
| ); | |
| } | |
| export default RouterView; |
This file contains hidden or 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 { fileURLToPath, URL } from "url"; | |
| export default defineConfig({ | |
| resolve: { | |
| alias: { | |
| "@router": fileURLToPath( | |
| new URL("./src/routing/router.jsx", import.meta.url) | |
| ), | |
| "@router/": fileURLToPath(new URL("./src/routing/", import.meta.url)), | |
| }, | |
| }, | |
| }); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
pages//index.jsx→/)/[param].jsx→:param)/[...param].jsx→:param*)/404.jsx)