import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
export const RouterContext = createContext(null);
export function RouterProvider({ children }) {
const [path, setPath] = useState(() => window.location.pathname);
useEffect(() => {
const onPopState = () => setPath(window.location.pathname);
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, []);
const navigate = useCallback((to, { replace = false } = {}) => {
if (to === window.location.pathname) return;
if (replace) {
window.history.replaceState({}, '', to);
} else {
window.history.pushState({}, '', to);
}
setPath(to);
}, []);
const value = useMemo(() => ({ path, navigate }), [path, navigate]);
return <RouterContext.Provider value={value}>{children}</RouterContext.Provider>;
}
import { useContext } from 'react';
import { RouterContext } from './RouterContext';
export function useRouter() {
const ctx = useContext(RouterContext);
if (!ctx) throw new Error('useRouter must be used within a <RouterProvider>');
return ctx;
}
function compilePattern(pattern) {
const keys = [];
const source = pattern
.replace(/\/+$/, '')
.replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
keys.push(key);
return '([^/]+)';
});
return { regex: new RegExp('^' + (source || '/') + '/?$'), keys };
}
export function matchPath(pattern, path) {
const { regex, keys } = compilePattern(pattern);
const match = regex.exec(path);
if (!match) return null;
const params = {};
keys.forEach((key, i) => {
params[key] = decodeURIComponent(match[i + 1]);
});
return { params };
}
export function useRoute(pattern) {
const { path } = useRouter();
return matchPath(pattern, path);
}
import { useRoute, useRouter } from './useRoute';
export function Route({ path, children, render }) {
const match = useRoute(path);
if (!match) return null;
if (typeof children === 'function') return children(match.params);
if (render) return render(match.params);
return children ?? null;
}
export function Link({ to, replace, onClick, children, ...rest }) {
const { navigate } = useRouter();
const handleClick = (event) => {
if (onClick) onClick(event);
const isPlainClick =
event.button === 0 &&
!event.metaKey &&
!event.ctrlKey &&
!event.shiftKey &&
!event.altKey &&
!rest.target;
if (!isPlainClick || event.defaultPrevented) return;
event.preventDefault();
navigate(to, { replace });
};
return (
<a href={to} onClick={handleClick} {...rest}>
{children}
</a>
);
}
This snippet builds a minimal client-side router for a React single-page app on top of the browser's History API, split into the pieces that actually make one work: a router provider that owns the current path, a hook that reads it, and route components that render conditionally against it.
In RouterContext, the router state lives in a single RouterProvider component. It holds the current path in state and subscribes to the browser's popstate event so back/forward buttons stay in sync with the UI. The navigate function is the write side: it calls history.pushState (or replaceState when replace is passed) and then updates React state, which is what actually re-renders the tree. Keeping pushState and setState together is the crux — the History API does not emit popstate for programmatic pushes, so state must be updated manually or navigation would silently do nothing visible. The context value is memoized with useMemo so consumers only re-render when path changes.
In useRoute hook, useRouter guards against being called outside the provider, and useRoute layers pattern matching on top. It converts a path pattern like /users/:id into a regular expression via compilePattern, extracting named segments into a params object. This gives components a declarative way to ask "does the current URL match, and if so what are the params?" without every component re-implementing string parsing. matchPath returns null on no match so callers can branch cleanly.
In Route and Link components, Route is a thin wrapper that renders its children (or an element via a render prop) only when useRoute(path) matches, injecting params when a function child is used. Link renders a real anchor for accessibility and correct middle-click/open-in-new-tab behavior, but intercepts plain left-clicks in handleClick to call navigate instead, calling preventDefault only for unmodified primary clicks. The trade-off of a router this small is no nested route matching or ranking, and exact-vs-prefix matching is left explicit — but it demonstrates the full data flow from URL to render with no external dependency. It is a good fit for tiny apps, demos, or understanding what larger routers do under the hood.
Related snips
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"
const application = Application.start()
application.debug = false
Disable submit button while Turbo form is submitting
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
url: String,
delay: { type: Number, default: 800 },
Stimulus: autosave draft with Turbo-friendly requests
Share this code
Here's the card — post it anywhere.