javascript 96 lines · 3 tabs

Client-Side Router with useRoute Hook and History API in React

Shared by codesnips Aug 2026
3 tabs
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>;
}
3 files · javascript Explain with highlit

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

python
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [

Django URL namespacing and reverse lookups

django python urls
by Priya Sharma 3 tabs
javascript
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

rails hotwire stimulus
by codesnips 4 tabs
typescript
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

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
javascript
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

rails hotwire stimulus
by codesnips 3 tabs
javascript
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

rails stimulus hotwire
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Client-Side Router with useRoute Hook and History API in React — share card
Link copied