Skip to content

Instantly share code, notes, and snippets.

@mofax
Created February 25, 2025 07:31
Show Gist options
  • Save mofax/3d9f97ab3e0b5b33768ba1ebf8b52b1f to your computer and use it in GitHub Desktop.
Save mofax/3d9f97ab3e0b5b33768ba1ebf8b52b1f to your computer and use it in GitHub Desktop.
Very simple react frontend router, built on tanstack store
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