Created
February 25, 2025 07:31
-
-
Save mofax/3d9f97ab3e0b5b33768ba1ebf8b52b1f to your computer and use it in GitHub Desktop.
Very simple react frontend router, built on tanstack store
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 React, { useEffect } from "react"; | |
import { Store } from "@tanstack/store"; | |
import { useStore } from "@tanstack/react-store"; | |
type RouteParams = Record<string, string>; | |
type QueryParams = URLSearchParams; | |
interface Route { | |
path: string; | |
component: React.ComponentType<{ params: RouteParams; query: QueryParams }>; | |
} | |
export const RouterStore = new Store({ | |
pathname: window.location.pathname, | |
query: new URLSearchParams(window.location.search) | |
}) | |
function setCurrentPath(path: string) { | |
RouterStore.setState((state) => ({ ...state, pathname: path })) | |
} | |
function setCurrentQuery(arg: URLSearchParams) { | |
RouterStore.setState((state) => ({ ...state, query: arg })) | |
} | |
export function useCurrentRouteInfo() { | |
const pathname = useStore(RouterStore, (state) => state.pathname); | |
const query = useStore(RouterStore, (state) => state.query); | |
return { | |
pathname, | |
query | |
} as const; | |
} | |
export const navigate = (path: string, query: Record<string, string> = {}) => { | |
const queryString = new URLSearchParams(query).toString(); | |
const fullPath = queryString ? `${path}?${queryString}` : path; | |
window.history.pushState({}, '', fullPath); | |
setCurrentPath(path); | |
setCurrentQuery(new URLSearchParams(queryString)); | |
}; | |
export const Router: React.FC<{ routes: Route[] }> = ({ routes }) => { | |
const info = useCurrentRouteInfo(); | |
const currentPath = info.pathname; | |
const currentQuery = info.query; | |
useEffect(() => { | |
const handlePopState = () => { | |
setCurrentPath(window.location.pathname); | |
setCurrentQuery(new URLSearchParams(window.location.search)); | |
}; | |
window.addEventListener('popstate', handlePopState); | |
return () => { | |
window.removeEventListener('popstate', handlePopState); | |
}; | |
}, []); | |
const matchRoute = (): Route | null => { | |
for (const route of routes) { | |
const pathMatch = matchPath(route.path, currentPath); | |
if (pathMatch) { | |
return route; | |
} | |
} | |
return null; | |
}; | |
const matchPath = (routePath: string, currentPath: string): RouteParams | null => { | |
const routeSegments = routePath.split('/').filter(Boolean); | |
const currentSegments = currentPath.split('/').filter(Boolean); | |
if (routeSegments.length !== currentSegments.length) { | |
return null; | |
} | |
const params: RouteParams = {}; | |
for (let i = 0; i < routeSegments.length; i++) { | |
const routeSegment = routeSegments[i]; | |
const currentSegment = currentSegments[i]; | |
if (routeSegment.startsWith(':')) { | |
const paramName = routeSegment.slice(1); | |
params[paramName] = currentSegment; | |
} else if (routeSegment !== currentSegment) { | |
return null; | |
} | |
} | |
return params; | |
}; | |
const route = matchRoute(); | |
if (route) { | |
const Component = route.component; | |
return <Component params={matchPath(route.path, currentPath) || {}} query={currentQuery} />; | |
} else { | |
return <div>No route matched</div>; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment