Repository: molefrog/wouter Branch: v3 Commit: 2061eacda7f8 Files: 93 Total size: 252.1 KB Directory structure: gitextract_4azr5gv8/ ├── .cursor/ │ └── rules/ │ ├── contributing.mdc │ └── publishing.mdc ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── ci-tests.yml │ └── size.yml ├── .gitignore ├── CLAUDE.md ├── LICENSE ├── README.md ├── bunfig.toml ├── package.json ├── packages/ │ ├── magazin/ │ │ ├── .gitignore │ │ ├── App.tsx │ │ ├── client.tsx │ │ ├── components/ │ │ │ ├── footer.tsx │ │ │ ├── navbar.tsx │ │ │ ├── star-wouter.tsx │ │ │ └── with-status-code.tsx │ │ ├── db/ │ │ │ └── products.ts │ │ ├── index.html │ │ ├── index.tsx │ │ ├── package.json │ │ ├── routes/ │ │ │ ├── 404.tsx │ │ │ ├── about.tsx │ │ │ ├── cart.tsx │ │ │ ├── home.tsx │ │ │ └── products/ │ │ │ └── [slug].tsx │ │ ├── styles.css │ │ └── tsconfig.json │ ├── wouter/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── memory-location.d.ts │ │ │ ├── memory-location.js │ │ │ ├── paths.js │ │ │ ├── react-deps.js │ │ │ ├── use-browser-location.d.ts │ │ │ ├── use-browser-location.js │ │ │ ├── use-hash-location.d.ts │ │ │ ├── use-hash-location.js │ │ │ ├── use-sync-external-store.js │ │ │ └── use-sync-external-store.native.js │ │ ├── test/ │ │ │ ├── history-patch.test.ts │ │ │ ├── jest-dom.d.ts │ │ │ ├── link.test-d.tsx │ │ │ ├── link.test.tsx │ │ │ ├── location-hook.test-d.ts │ │ │ ├── match-route.test-d.ts │ │ │ ├── memory-location.test-d.ts │ │ │ ├── memory-location.test.ts │ │ │ ├── nested-route.test.tsx │ │ │ ├── parser.test.tsx │ │ │ ├── redirect.test-d.tsx │ │ │ ├── redirect.test.tsx │ │ │ ├── route.test-d.tsx │ │ │ ├── route.test.tsx │ │ │ ├── router.test-d.tsx │ │ │ ├── router.test.tsx │ │ │ ├── setup.ts │ │ │ ├── ssr.test.tsx │ │ │ ├── switch.test.tsx │ │ │ ├── test-utils.ts │ │ │ ├── use-browser-location.test-d.ts │ │ │ ├── use-browser-location.test.tsx │ │ │ ├── use-hash-location.test-d.ts │ │ │ ├── use-hash-location.test.tsx │ │ │ ├── use-location.test.tsx │ │ │ ├── use-params.test-d.ts │ │ │ ├── use-params.test.tsx │ │ │ ├── use-route.test-d.ts │ │ │ ├── use-route.test.tsx │ │ │ ├── use-search-params.test.tsx │ │ │ ├── use-search.test.tsx │ │ │ └── view-transitions.test.tsx │ │ └── types/ │ │ ├── index.d.ts │ │ ├── location-hook.d.ts │ │ ├── memory-location.d.ts │ │ ├── router.d.ts │ │ ├── use-browser-location.d.ts │ │ └── use-hash-location.d.ts │ └── wouter-preact/ │ ├── .gitignore │ ├── package.json │ ├── src/ │ │ └── react-deps.js │ ├── test/ │ │ └── preact.test.tsx │ └── types/ │ ├── index.d.ts │ ├── location-hook.d.ts │ ├── memory-location.d.ts │ ├── router.d.ts │ ├── use-browser-location.d.ts │ └── use-hash-location.d.ts ├── specs/ │ └── view-transitions-spec.md └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cursor/rules/contributing.mdc ================================================ --- description: globs: alwaysApply: true --- # Wouter - Minimalist React Router ## Project Overview Wouter is a **minimalist-friendly ~2.1KB React router** that focuses on performance, small bundle size, and simplicity. It provides hook-based routing for React and Preact applications. ### Key Features - Tiny bundle size (2.1KB gzipped vs 18.7KB React Router) - Hook-based API (`useLocation`, `useRoute`, `useParams`, etc.) - Zero dependencies - Supports React and Preact - Optional top-level `` component - Nested routing support - SSR support - Memory location for testing ## Project Structure ``` wouter/ ├── packages/ │ ├── wouter/ # Main React package │ │ ├── src/ │ │ │ ├── index.js # Main router implementation │ │ │ ├── use-browser-location.js │ │ │ ├── use-hash-location.js │ │ │ ├── memory-location.js │ │ │ ├── react-deps.js # React imports │ │ │ └── paths.js # Path utilities │ │ ├── test/ # Test files │ │ ├── types/ # TypeScript definitions │ │ └── esm/ # Built ES modules │ └── wouter-preact/ # Preact package ├── assets/ # Documentation assets └── .github/ # GitHub workflows ``` ## Development Workflow ### Prerequisites - Node.js v20.17.0+ or v22.9.0+ - npm ### Commands ```bash # Install dependencies npm install # Run tests (watch mode) npm test # Run tests (single run) npm test -- --run # Run specific test file npm test -- packages/wouter/test/router.test.tsx --run # Run tests with coverage npm test -- --coverage # Build packages npm run build # Type checking npm run typecheck # Lint code npm run lint ``` ### Test Files Organization - `test/router.test.tsx` - Router component and memoization tests - `test/use-location.test.tsx` - Location hook tests - `test/use-route.test.tsx` - Route matching tests - `test/link.test.tsx` - Link component tests - `test/memory-location.test.ts` - Memory location for testing - `test/nested-route.test.tsx` - Nested routing tests - `test/ssr.test.tsx` - Server-side rendering tests ## Key Development Principles ### Performance First - **Bundle size matters**: Every byte counts. Use size-limit CI checks - **Stable object references**: Prevent unnecessary re-renders with proper memoization - **Minimize dependencies**: Zero external dependencies policy ### Code Style - **Functional programming**: Prefer hooks over class components - **Compact code**: Optimize for bundle size (may sacrifice readability) - **React patterns**: Use `React.memo`, `useCallback`, `useMemo` appropriately ### Testing Guidelines - **Comprehensive coverage**: Test all public APIs - **Real-world scenarios**: Include complex use cases (like language switching) - **Memory location**: Use for isolated testing - **Memoization tests**: Verify stable object references ## Core APIs ### Hooks - `useLocation()` - Get/set current location - `useRoute(pattern)` - Match current location against pattern - `useParams()` - Extract route parameters - `useSearch()` - Get search/query string - `useSearchParams()` - Get/set URLSearchParams - `useRouter()` - Access router configuration ### Components - `` - Optional configuration wrapper - `` - Conditional rendering based on path - `` - Navigation component - `` - Exclusive routing - `` - Programmatic navigation ### Location Hooks - `useBrowserLocation` - Browser history API - `useHashLocation` - Hash-based routing - `memoryLocation` - In-memory for testing ## Performance Considerations ### Router Memoization The Router component uses sophisticated memoization to prevent unnecessary re-renders: - Only creates new router objects when props actually change - Preserves stable references for `React.memo` components - Critical for performance in complex applications ### Bundle Optimization - Use `// ... existing code ...` pattern in edits to minimize diffs - Prefer shorter variable names in production builds - Optimize for gzip/brotli compression ## Common Patterns ### Testing Router Components ```javascript import { memoryLocation } from "wouter/memory-location"; const { hook } = memoryLocation({ path: "/test/path" }); render( ); ``` ### Custom Location Hooks ```javascript const useCustomLocation = () => { const [location, setLocation] = useBrowserLocation(); // Add custom logic here return [location, setLocation]; }; ``` ### Nested Routing ```javascript ``` ## Debugging Tips ### Common Issues 1. **Router memoization**: Check if unnecessary re-renders occur 2. **Base path conflicts**: Verify base path inheritance in nested routers 3. **Pattern matching**: Use regexparam syntax for route patterns 4. **SSR hydration**: Ensure client/server router configs match ### Debugging Tools - React DevTools Profiler for render tracking - Network tab for bundle size verification - Console warnings for deprecated patterns ## Contributing Guidelines ### Before Making Changes 1. Run full test suite: `npm test -- --run` 2. Check bundle size impact 3. Verify TypeScript definitions 4. Test with both React and Preact ### Adding New Features 1. Consider bundle size impact 2. Add comprehensive tests 3. Update TypeScript definitions 4. Document in README if public API 5. Maintain backward compatibility ### Performance Changes 1. Add before/after performance tests 2. Verify memoization behavior 3. Test with complex nested scenarios 4. Check for memory leaks in long-running apps ## Architecture Notes ### Router Context System - Default router created on-demand - Context inheritance with override capabilities - Memoization prevents cascade re-renders - Base path accumulation in nested routers ### Pattern Matching - Uses regexparam library internally - Supports named parameters, wildcards, optional segments - Regex patterns supported for complex matching - Loose mode for nested routing This project prioritizes performance and minimalism while maintaining full feature parity with larger routing solutions. ================================================ FILE: .cursor/rules/publishing.mdc ================================================ --- description: globs: alwaysApply: true --- # Publishing Wouter Packages This document outlines the process for publishing new versions of the wouter packages to npm. ## Prerequisites - Ensure you have npm publish permissions for both `wouter` and `wouter-preact` packages - Have npm authentication set up (you'll need OTP access) - All tests should be passing - All changes should be committed to the repository ## Publishing Process ### 1. Version Bump Update the version in both package.json files: - `packages/wouter/package.json` - `packages/wouter-preact/package.json` For semantic versioning: - **Patch** (x.x.X): Bug fixes, small improvements - **Minor** (x.X.x): New features, backward compatible - **Major** (X.x.x): Breaking changes ### 2. Dry Run Validation Run dry publish commands to validate the packages before actual publishing: ```bash npm publish --dry-run -w wouter npm publish --dry-run -w wouter-preact ``` This will show you: - Package contents and file list - Bundle size information - Version confirmation - Any potential issues ### 3. Build Verification (Optional) Check the build artifacts in the `/esm/` folders to ensure the latest updates are properly built: - `packages/wouter/esm/` - `packages/wouter-preact/esm/` The `prepublishOnly` script automatically builds the packages, but it's good to verify. ## ⚠️ Confirmation Required **ALWAYS ask for user confirmation before proceeding with the following steps:** ### 4. Publish to npm Publish both packages to the npm registry: ```bash npm publish -w wouter npm publish -w wouter-preact ``` **Note:** npm might fail with "EOTP" (Error One-Time Password) even when the OTP was entered correctly in the browser. This is a known npm issue - the packages are usually still published successfully despite the error message. ### 5. Git Commit and Tag After successful publishing: 1. **Commit the version changes:** ```bash git add packages/wouter/package.json packages/wouter-preact/package.json git commit -m "Bump version to X.X.X" ``` 2. **Create a version tag:** ```bash git tag vX.X.X ``` 3. **Push to remote (optional):** ```bash git push origin main git push origin vX.X.X ``` ## Troubleshooting ### Common Issues - **Authentication errors**: Run `npm login` or use the browser authentication flow - **OTP timeout**: Generate a fresh OTP code from your authenticator app - **Permission denied**: Ensure you have publish permissions for both packages - **Version conflicts**: Check if the version already exists on npm ### Verification After publishing, verify the packages are available: - Check [wouter on npm](mdc:https:/www.npmjs.com/package/wouter) - Check [wouter-preact on npm](mdc:https:/www.npmjs.com/package/wouter-preact) - Test installation: `npm install wouter@latest` ## Package Information - **wouter**: Main React package (~22KB package size) - **wouter-preact**: Preact-specific package (~22KB package size) - Both packages maintain version parity and should be published together ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: ["molefrog"] ================================================ FILE: .github/workflows/ci-tests.yml ================================================ name: Run Tests & Linters on: push: branches: "*" pull_request: branches: "*" jobs: build: runs-on: ubuntu-latest env: FORCE_COLOR: true steps: - uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install Dependencies run: bun install --frozen-lockfile - name: Prepare wouter-preact (copy source files) run: cd packages/wouter-preact && npm run prepublishOnly - name: Run test run: bun test --coverage --coverage-reporter=lcov --coverage-reporter=text - name: Run type check run: bun run test-types - name: Lint Sources with ESLint run: bun run lint - name: Upload Coverage to Coveralls uses: coverallsapp/github-action@v2 with: file: ./coverage/lcov.info ================================================ FILE: .github/workflows/size.yml ================================================ name: Size on: [pull_request] jobs: size: runs-on: ubuntu-latest env: CI_JOB_NUMBER: 1 steps: - uses: actions/checkout@v3 - uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install Dependencies run: bun install --frozen-lockfile - name: Prepare wouter-preact (copy source files) run: cd packages/wouter-preact && npm run prepublishOnly - name: Symlink npm to bun run: | sudo ln -sf $(which bun) /usr/local/bin/npm sudo ln -sf $(which bunx) /usr/local/bin/npx - uses: andresz1/size-limit-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} skip_step: install build_script: build ================================================ FILE: .gitignore ================================================ # NPM npm-debug.log* node_modules/ .npm # IDEs .vscode/ *.code-workspace # bundler esm/ .cache # OSX .DS_Store .AppleDouble .LSOverride # test coverage coverage/ # type definitions in the project root are copied from types/ folder # before publishing, so they should be ignored /*.d.ts # README is copied from the root folder packages/wouter/README.md packages/wouter-preact/README.md ================================================ FILE: CLAUDE.md ================================================ --- description: Use Bun instead of Node.js, npm, pnpm, or vite. globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" alwaysApply: false --- Default to using Bun instead of Node.js. - Use `bun ` instead of `node ` or `ts-node ` - Use `bun test` instead of `jest` or `vitest` - Use `bun build ` instead of `webpack` or `esbuild` - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` - Use `bun run ``` With the following `frontend.tsx`: ```tsx#frontend.tsx import React from "react"; // import .css files directly and it works import './index.css'; import { createRoot } from "react-dom/client"; const root = createRoot(document.body); export default function Frontend() { return

Hello, world!

; } root.render(); ``` Then, run index.ts ```sh bun --hot ./index.ts ``` For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. ================================================ FILE: LICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: README.md ================================================
Wouter — a super-tiny React router (logo by Katya Simacheva)

npm CI Coverage Coverage Edit in StackBlitz IDE
wouter is a tiny router for modern React and Preact apps that relies on Hooks.
A router you wanted so bad in your project!
## Features by Katya Simacheva - Minimum dependencies, only **2.1 KB** gzipped vs 18.7KB [React Router](https://github.com/ReactTraining/react-router). - Supports both **React** and **[Preact](https://preactjs.com/)**! Read _["Preact support" section](#preact-support)_ for more details. - No top-level `` component, it is **fully optional**. - Mimics [React Router](https://github.com/ReactTraining/react-router)'s best practices by providing familiar **[`Route`](#route-pathpattern-)**, **[`Link`](#link-hrefpath-)**, **[`Switch`](#switch-)** and **[`Redirect`](#redirect-topath-)** components. - Has hook-based API for more granular control over routing (like animations): **[`useLocation`](#uselocation-working-with-the-history)**, **[`useRoute`](#useroute-route-matching-and-parameters)** and **[`useRouter`](#userouter-accessing-the-router-object)**. ## developers :sparkling_heart: wouter > ... I love Wouter. It’s tiny, fully embraces hooks, and has an intuitive and barebones API. I can > accomplish everything I could with react-router with Wouter, and it just feels **more minimalist > while not being inconvenient.** > > [**Matt Miller**, _An exhaustive React ecosystem for 2020_](https://medium.com/@mmiller42/an-exhaustive-react-guide-for-2020-7859f0bddc56) Wouter provides a simple API that many developers and library authors appreciate. Some notable projects that use wouter: **[Ultra](https://ultrajs.dev/)**, **[React-three-fiber](https://github.com/react-spring/react-three-fiber)**, **[Sunmao UI](https://sunmao-ui.com/)**, **[Million](https://million.dev/)** and many more. ## Table of Contents - [Getting Started](#getting-started) - [Browser Support](#browser-support) - [Wouter API](#wouter-api) - [The list of methods available](#the-list-of-methods-available) - [Hooks API](#hooks-api) - [`useRoute`: route matching and parameters](#useroute-route-matching-and-parameters) - [`useLocation`: working with the history](#uselocation-working-with-the-history) - [Additional navigation parameters](#additional-navigation-parameters) - [Customizing the location hook](#customizing-the-location-hook) - [`useParams`: extracting matched parameters](#useparams-extracting-matched-parameters) - [`useSearch`: query strings](#usesearch-query-strings) - [`useSearchParams`: search parameters](#usesearchparams-search-parameters) - [`useRouter`: accessing the router object](#userouter-accessing-the-router-object) - [Component API](#component-api) - [``](#route-pathpattern-) - [Route nesting](#route-nesting) - [``](#link-hrefpath-) - [``](#switch-) - [``](#redirect-topath-) - [``](#router-hookhook-parserfn-basebasepath-hrefsfn-) - [FAQ and Code Recipes](#faq-and-code-recipes) - [I deploy my app to the subfolder. Can I specify a base path?](#i-deploy-my-app-to-the-subfolder-can-i-specify-a-base-path) - [How do I make a default route?](#how-do-i-make-a-default-route) - [How do I make a link active for the current route?](#how-do-i-make-a-link-active-for-the-current-route) - [Are strict routes supported?](#are-strict-routes-supported) - [Are relative routes and links supported?](#are-relative-routes-and-links-supported) - [Can I initiate navigation from outside a component?](#can-i-initiate-navigation-from-outside-a-component) - [Can I use _wouter_ in my TypeScript project?](#can-i-use-wouter-in-my-typescript-project) - [How can add animated route transitions?](#how-can-add-animated-route-transitions) - [How do I add view transitions to my app?](#how-do-i-add-view-transitions-to-my-app) - [Preact support?](#preact-support) - [Server-side Rendering support (SSR)?](#server-side-rendering-support-ssr) - [How do I configure the router to render a specific route in tests?](#how-do-i-configure-the-router-to-render-a-specific-route-in-tests) - [1KB is too much, I can't afford it!](#1kb-is-too-much-i-cant-afford-it) - [Acknowledgements](#acknowledgements) ## Getting Started First, add wouter to your project. ```bash npm i wouter ``` Or, if you're using Preact the use the following command [`npm i wouter-preact`](#preact-support). Check out this simple demo app below. It doesn't cover hooks and other features such as nested routing, but it's a good starting point for those who are migrating from React Router. ```js import { Link, Route, Switch } from "wouter"; const App = () => ( <> Profile About Us {/* Routes below are matched exclusively - the first matched route gets rendered */} {(params) => <>Hello, {params.name}!} {/* Default route in a switch */} 404: No such page! ); ``` ### Browser Support This library is designed for **ES2020+** compatibility. If you need to support older browsers, make sure that you transpile `node_modules`. Additionally, the minimum supported TypeScript version is 4.1 in order to support route parameter inference. ## Wouter API Wouter comes with three kinds of APIs: low-level **standalone location hooks**, hooks for **routing and pattern matching** and more traditional **component-based API** similar to React Router's one. You are free to choose whatever works for you: use location hooks when you want to keep your app as small as possible and don't need pattern matching; use routing hooks when you want to build custom routing components; or if you're building a traditional app with pages and navigation — components might come in handy. Check out also [FAQ and Code Recipes](#faq-and-code-recipes) for more advanced things like active links, default routes, server-side rendering etc. ### The list of methods available **Location Hooks** These can be used separately from the main module and have an interface similar to `useState`. These hooks are standalone and don't include built-in support for nesting, base path, or route matching. However, when passed to ``, they work seamlessly with all Router features including nesting and base paths. - **[`import { useBrowserLocation } from "wouter/use-browser-location"`](https://github.com/molefrog/wouter/blob/v3/packages/wouter/src/use-browser-location.js)** — allows to manipulate current location in the browser's address bar, a tiny wrapper around the History API. - **[`import { useHashLocation } from "wouter/use-hash-location"`](https://github.com/molefrog/wouter/blob/v3/packages/wouter/src/use-hash-location.js)** — similarly, gets location from the hash part of the address, i.e. the string after a `#`. - **[`import { memoryLocation } from "wouter/memory-location"`](#uselocation-working-with-the-history)** — an in-memory location hook with history support, external navigation and immutable mode for testing. **Note** the module name because it is a high-order hook. See how memory location can be used in [testing](#how-do-i-configure-the-router-to-render-a-specific-route-in-tests). **Routing Hooks** Import from `wouter` module. - **[`useRoute`](#useroute-the-power-of-hooks)** — shows whether or not current page matches the pattern provided. - **[`useLocation`](#uselocation-working-with-the-history)** — allows to manipulate current router's location, by default subscribes to browser location. **Note:** this isn't the same as `useBrowserLocation`, read below. - **[`useParams`](#useparams-extracting-matched-parameters)** — returns an object with parameters matched from the closest route. - **[`useSearch`](#usesearch-query-strings)** — returns a search string – everything that goes after the `?`. - **[`useRouter`](#userouter-accessing-the-router-object)** — returns a global router object that holds the configuration. Only use it if you want to customize the routing. **Components** Import from `wouter` module. - **[``](#route-pathpattern-)** — conditionally renders a component based on a pattern. - **[``](#link-hrefpath-)** — wraps ``, allows to perform a navigation. - **[``](#switch-)** — exclusive routing, only renders the first matched route. - **[``](#redirect-topath-)** — when rendered, performs an immediate navigation. - **[``](#router-hookhook-matchermatchfn-basebasepath-)** — an optional top-level component for advanced routing configuration. ## Hooks API ### `useRoute`: route matching and parameters Checks if the current location matches the pattern provided and returns an object with parameters. This is powered by a wonderful [`regexparam`](https://github.com/lukeed/regexparam) library, so all its pattern syntax is fully supported. You can use `useRoute` to perform manual routing or implement custom logic, such as route transitions, etc. ```js import { useRoute } from "wouter"; const Users = () => { // `match` is a boolean const [match, params] = useRoute("/users/:name"); if (match) { return <>Hello, {params.name}!; } else { return null; } }; ``` A quick cheatsheet of what types of segments are supported: ```js useRoute("/app/:page"); useRoute("/app/:page/:section"); // optional parameter, matches "/en/home" and "/home" useRoute("/:locale?/home"); // suffixes useRoute("/movies/:title.(mp4|mov)"); // wildcards, matches "/app", "/app-1", "/app/home" useRoute("/app*"); // optional wildcards, matches "/orders", "/orders/" // and "/orders/completed/list" useRoute("/orders/*?"); // regex for matching complex patterns, // matches "/hello:123" useRoute(/^[/]([a-z]+):([0-9]+)[/]?$/); // and with named capture groups useRoute(/^[/](?[a-z]+):(?[0-9]+)[/]?$/); ``` The second item in the pair `params` is an object with parameters or null if there was no match. For wildcard segments the parameter name is `"*"`: ```js // wildcards, matches "/app", "/app-1", "/app/home" const [match, params] = useRoute("/app*"); if (match) { // "/home" for "/app/home" const page = params["*"]; } ``` ### `useLocation`: working with the history To get the current path and navigate between pages, call the `useLocation` hook. Similarly to `useState`, it returns a value and a setter: the component will re-render when the location changes and by calling `navigate` you can update this value and perform navigation. By default, it uses `useBrowserLocation` under the hood, though you can configure this in a top-level `Router` component (for example, if you decide at some point to switch to a hash-based routing). `useLocation` will also return scoped path when used within nested routes or with base path setting. ```js import { useLocation } from "wouter"; const CurrentLocation = () => { const [location, navigate] = useLocation(); return ( ); }; ``` All the components internally call the `useLocation` hook. #### Additional navigation parameters The setter method of `useLocation` can also accept an optional object with parameters to control how the navigation update will happen. When browser location is used (default), `useLocation` hook accepts `replace` flag to tell the hook to modify the current history entry instead of adding a new one. It is the same as calling `replaceState`. ```jsx const [location, navigate] = useLocation(); navigate("/jobs"); // `pushState` is used navigate("/home", { replace: true }); // `replaceState` is used ``` Additionally, you can provide a `state` option to update `history.state` while navigating: ```jsx navigate("/home", { state: { modal: "promo" } }); history.state; // { modal: "promo" } ``` #### Customizing the location hook By default, **wouter** uses `useLocation` hook that reacts to `pushState` and `replaceState` navigation via `useBrowserLocation`. To customize this, wrap your app in a `Router` component: ```js import { Router, Route } from "wouter"; import { useHashLocation } from "wouter/use-hash-location"; const App = () => ( ... ); ``` Because these hooks have return values similar to `useState`, it is easy and fun to build your own location hooks: `useCrossTabLocation`, `useLocalStorage`, `useMicroFrontendLocation` and whatever routing logic you want to support in the app. Give it a try! ### `useParams`: extracting matched parameters This hook allows you to access the parameters exposed through [matching dynamic segments](#matching-dynamic-segments). Internally, we simply wrap your components in a context provider allowing you to access this data anywhere within the `Route` component. This allows you to avoid "prop drilling" when dealing with deeply nested components within the route. **Note:** `useParams` will only extract parameters from the closest parent route. ```js import { Route, useParams } from "wouter"; const User = () => { const params = useParams(); params.id; // "1" // alternatively, use the index to access the prop params[0]; // "1" }; /> ``` It is the same for regex paths. Capture groups can be accessed by their index, or if there is a named capture group, that can be used instead. ```js import { Route, useParams } from "wouter"; const User = () => { const params = useParams(); params.id; // "1" params[0]; // "1" }; [0-9]+)[/]?$/} component={User}> /> ``` ### `useSearch`: query strings Use this hook to get the current search (query) string value. It will cause your component to re-render only when the string itself and not the full location updates. The search string returned **does not** contain a `?` character. ```jsx import { useSearch } from "wouter"; // returns "tab=settings&id=1" const searchString = useSearch(); ``` For the SSR, use `ssrSearch` prop passed to the router. ```jsx {/* SSR! */} ``` Refer to [Server-Side Rendering](#server-side-rendering-support-ssr) for more info on rendering and hydration. ### `useSearchParams`: search parameters Returns a `URLSearchParams` object and a setter function to update search parameters. The setter accepts either a value (object, URLSearchParams, string[][], etc.) or a **callback function** that receives the current params and must return the new params. ```jsx import { useSearchParams } from 'wouter'; const [searchParams, setSearchParams] = useSearchParams(); // extract a specific search parameter const id = searchParams.get('id'); // modify a specific search parameter setSearchParams((prev) => { prev.set('tab', 'settings'); return prev; }); // override all search parameters setSearchParams({ id: 1234, tab: 'settings', }); // by default, setSearchParams() will push a new history entry // to avoid this, set `replace` option to `true` setSearchParams( (prev) => { prev.set('order', 'desc'); return prev; }, { replace: true, }, ); // you can also pass a history state in options setSearchParams( (prev) => { prev.set('foo', 'bar'); return prev; }, { state: 'hello', }, ); ``` ### `useRouter`: accessing the router object If you're building advanced integration, for example custom location hook, you might want to get access to the global router object. Router is a simple object that holds routing options that you configure in the `Router` component. ```js import { useRouter } from "wouter"; const Custom = () => { const router = useRouter(); router.hook; // `useBrowserLocation` by default router.base; // "/app" }; const App = () => ( ); ``` ## Component API ### `` `Route` represents a piece of the app that is rendered conditionally based on a pattern `path`. Pattern has the same syntax as the argument you pass to [`useRoute`](#useroute-route-matching-and-parameters). The library provides multiple ways to declare a route's body: ```js import { Route } from "wouter"; // simple form // render-prop style {params => } // the `params` prop will be passed down to ``` A route with no path is considered to always match, and it is the same as ``. When developing your app, use this trick to peek at the route's content without navigation. ```diff - + {/* Strip out the `path` to make this visible */} ``` #### Route Nesting Nesting is a core feature of wouter and can be enabled on a route via the `nest` prop. When this prop is present, the route matches everything that starts with a given pattern and it creates a nested routing context. All child routes will receive location relative to that pattern. Let's take a look at this example: ```js ``` 1. This first route will be active for all paths that start with `/app`, this is equivalent to having a base path in your app. 2. The second one uses dynamic pattern to match paths like `/app/user/1`, `/app/user/1/anything` and so on. 3. Finally, the inner-most route will only work for paths that look like `/app/users/1/orders`. The match is strict, since that route does not have a `nest` prop and it works as usual. If you call `useLocation()` inside the last route, it will return `/orders` and not `/app/users/1/orders`. This creates a nice isolation and it makes it easier to make changes to parent route without worrying that the rest of the app will stop working. If you need to navigate to a top-level page however, you can use a prefix `~` to refer to an absolute path: ```js Back to Home ``` **Note:** The `nest` prop does not alter the regex passed into regex paths. Instead, the `nest` prop will only determine if nested routes will match against the rest of path or the same path. To make a strict path regex, use a regex pattern like `/^[/](your pattern)[/]?$/` (this matches an optional end slash and the end of the string). To make a nestable regex, use a regex pattern like `/^[/](your pattern)(?=$|[/])/` (this matches either the end of the string or a slash for future segments). ### `` Link component renders an `` element that, when clicked, performs a navigation. ```js import { Link } from "wouter" Home // `to` is an alias for `href` Home // all standard `a` props are proxied Home // all location hook options are supported ``` Link will always wrap its children in an `` tag, unless `asChild` prop is provided. Use this when you need to have a custom component that renders an `` under the hood. ```jsx // use this instead // Remember, `UIKitLink` must implement an `onClick` handler // in order for navigation to work! ``` When you pass a function as a `className` prop, it will be called with a boolean value indicating whether the link is active for the current route. You can use this to style active links (e.g. for links in navigation menu) ```jsx (active ? "active" : "")}>Nav ``` Read more about [active links here](#how-do-i-make-a-link-active-for-the-current-route). ### `` There are cases when you want to have an exclusive routing: to make sure that only one route is rendered at the time, even if the routes have patterns that overlap. That's what `Switch` does: it only renders **the first matching route**. ```js import { Route, Switch } from "wouter"; {/* in wouter, any Route with empty path is considered always active. This can be used to achieve "default" route behaviour within Switch. Note: the order matters! See examples below. */} This is rendered when nothing above has matched ; ``` When no route in switch matches, the last empty `Route` will be used as a fallback. See [**FAQ and Code Recipes** section](#how-do-i-make-a-default-route) to read about default routes. ### `` When mounted performs a redirect to a `path` provided. Uses `useLocation` hook internally to trigger the navigation inside of a `useEffect` block. `Redirect` can also accept props for [customizing how navigation will be performed](#additional-navigation-parameters), for example for setting history state when navigating. These options are specific to the currently used location hook. ```jsx // arbitrary state object // use `replaceState` ``` If you need more advanced logic for navigation, for example, to trigger the redirect inside of an event handler, consider using [`useLocation` hook instead](#uselocation-working-with-the-history): ```js import { useLocation } from "wouter"; const [location, setLocation] = useLocation(); fetchOrders().then((orders) => { setOrders(orders); setLocation("/app/orders"); }); ``` ### `` Unlike _React Router_, routes in wouter **don't have to be wrapped in a top-level component**. An internal router object will be constructed on demand, so you can start writing your app without polluting it with a cascade of top-level providers. There are cases however, when the routing behaviour needs to be customized. These cases include hash-based routing, basepath support, custom matcher function etc. ```jsx import { useHashLocation } from "wouter/use-hash-location"; {/* Your app goes here */} ; ``` A router is a simple object that holds the routing configuration options. You can always obtain this object using a [`useRouter` hook](#userouter-accessing-the-router-object). The list of currently available options: - **`hook: () => [location: string, setLocation: fn]`** — is a React Hook function that subscribes to location changes. It returns a pair of current `location` string e.g. `/app/users` and a `setLocation` function for navigation. You can use this hook from any component of your app by calling [`useLocation()` hook](#uselocation-working-with-the-history). See [Customizing the location hook](#customizing-the-location-hook). - **`searchHook: () => [search: string, setSearch: fn]`** — similar to `hook`, but for obtaining the [current search string](#usesearch-query-strings). - **`base: string`** — an optional setting that allows to specify a base path, such as `/app`. All application routes will be relative to that path. To navigate out to an absolute path, prefix your path with an `~`. [See the FAQ](#are-relative-routes-and-links-supported). - **`parser: (path: string, loose?: boolean) => { pattern, keys }`** — a pattern parsing function. Produces a RegExp for matching the current location against the user-defined patterns like `/app/users/:id`. Has the same interface as the [`parse`](https://github.com/lukeed/regexparam?tab=readme-ov-file#regexparamparseinput-regexp) function from `regexparam`. See [this example](#are-strict-routes-supported) that demonstrates custom parser feature. - **`ssrPath: string`** and **`ssrSearch: string`** use these when [rendering your app on the server](#server-side-rendering-support-ssr). - `hrefs: (href: boolean) => string` — a function for transforming `href` attribute of an `` element rendered by `Link`. It is used to support hash-based routing. By default, `href` attribute is the same as the `href` or `to` prop of a `Link`. A location hook can also define a `hook.hrefs` property, in this case the `href` will be inferred. - **`aroundNav: (navigate, to, options) => void`** — a handler that wraps all navigation calls. Use this to intercept navigation and perform custom logic before and after the navigation occurs. You can modify navigation parameters, add side effects, or prevent navigation entirely. This is particularly useful for implementing [view transitions](#how-do-i-add-view-transitions-to-my-app). By default, it simply calls `navigate(to, options)`. ```js const aroundNav = (navigate, to, options) => { // do something before navigation navigate(to, options); // perform navigation // do something after navigation }; ``` ## FAQ and Code Recipes ### I deploy my app to the subfolder. Can I specify a base path? You can! Wrap your app with `` component and that should do the trick: ```js import { Router, Route, Link } from "wouter"; const App = () => ( {/* the link's href attribute will be "/app/users" */} Users The current path is /app/users! ); ``` Calling `useLocation()` within a route in an app with base path will return a path scoped to the base. Meaning that when base is `"/app"` and pathname is `"/app/users"` the returned string is `"/users"`. Accordingly, calling `navigate` will automatically append the base to the path argument for you. When you have multiple nested routers, base paths are inherited and stack up. ```js Path is /app/cms/users! ``` ### How do I make a default route? One of the common patterns in application routing is having a default route that will be shown as a fallback, in case no other route matches (for example, if you need to render 404 message). In **wouter** this can easily be done as a combination of `` component and a default route: ```js import { Switch, Route } from "wouter"; ... 404, Not Found! ; ``` _Note:_ the order of switch children matters, default route should always come last. If you want to have access to the matched segment of the path you can use wildcard parameters: ```js ... {/* will match anything that starts with /users/, e.g. /users/foo, /users/1/edit etc. */} ... {/* will match everything else */} {(params) => `404, Sorry the page ${params["*"]} does not exist!`} ``` **[▶ Demo Sandbox](https://codesandbox.io/s/wouter-v3-ts-8q532r)** ### How do I make a link active for the current route? Instead of a regular `className` string, provide a function to use custom class when this link matches the current route. Note that it will always perform an exact match (i.e. `/users` will not be active for `/users/1`). ```jsx (active ? "active" : "")}>Nav link ``` If you need to control other props, such as `aria-current` or `style`, you can write your own `` wrapper and detect if the path is active by using the `useRoute` hook. ```js const [isActive] = useRoute(props.href); return ( {props.children} ); ``` **[▶ Demo Sandbox](https://codesandbox.io/s/wouter-v3-ts-8q532r?file=/src/ActiveLink.tsx)** ### Are strict routes supported? If a trailing slash is important for your app's routing, you could specify a custom parser. Parser is a method that takes a pattern string and returns a RegExp and an array of parsed key. It uses the signature of a [`parse`](https://github.com/lukeed/regexparam?tab=readme-ov-file#regexparamparseinput-regexp) function from `regexparam`. Let's write a custom parser based on a popular [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) package that does support strict routes option. ```js import { pathToRegexp } from "path-to-regexp"; /** * Custom parser based on `pathToRegexp` with strict route option */ const strictParser = (path, loose) => { const keys = []; const pattern = pathToRegexp(path, keys, { strict: true, end: !loose }); return { pattern, // `pathToRegexp` returns some metadata about the keys, // we want to strip it to just an array of keys keys: keys.map((k) => k.name), }; }; const App = () => ( ... ... ); ``` **[▶ Demo Sandbox](https://codesandbox.io/p/sandbox/wouter-v3-strict-routes-w3xdtz)** ### Are relative routes and links supported? Yes! Any route with `nest` prop present creates a nesting context. Keep in mind, that the location inside a nested route will be scoped. ```js const App = () => ( {/* the href is "/app/dashboard/users" */} {/* Here `useLocation()` returns "/users"! */} ); ``` **[▶ Demo Sandbox](https://codesandbox.io/p/sandbox/wouter-v3-nested-routes-l8p23s)** ### Can I initiate navigation from outside a component? Yes, the `navigate` function is exposed from the `"wouter/use-browser-location"` module: ```js import { navigate } from "wouter/use-browser-location"; navigate("/", { replace: true }); ``` It's the same function that is used internally. ### Can I use _wouter_ in my TypeScript project? Yes! Although the project isn't written in TypeScript, the type definition files are bundled with the package. ### How can add animated route transitions? Let's take look at how wouter routes can be animated with [`framer-motion`](framer.com/motion). Animating enter transitions is easy, but exit transitions require a bit more work. We'll use the `AnimatePresence` component that will keep the page in the DOM until the exit animation is complete. Unfortunately, `AnimatePresence` only animates its **direct children**, so this won't work: ```jsx import { motion, AnimatePresence } from "framer-motion"; export const MyComponent = () => ( {/* This will not work! `motion.div` is not a direct child */} ); ``` The workaround is to match this route manually with `useRoute`: ```jsx export const MyComponent = ({ isVisible }) => { const [isMatch] = useRoute("/"); return ( {isMatch && ( )} ); }; ``` More complex examples involve using `useRoutes` hook (similar to how React Router does it), but wouter does not ship it out-of-the-box. Please refer to [this issue](https://github.com/molefrog/wouter/issues/414#issuecomment-1954192679) for the workaround. ### How do I use wouter with View Transitions API? Wouter works seamlessly with the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API), but you'll need to manually activate it. This is because view transitions require synchronous DOM rendering and must be wrapped in `flushSync` from `react-dom`. Following wouter's philosophy of staying lightweight and avoiding unnecessary dependencies, view transitions aren't built-in. However, there's a simple escape hatch to enable them: the `aroundNav` prop. ```jsx import { flushSync } from "react-dom"; import { Router, type AroundNavHandler } from "wouter"; const aroundNav: AroundNavHandler = (navigate, to, options) => { // Check if View Transitions API is supported if (!document.startViewTransition) { navigate(to, options); return; } document.startViewTransition(() => { flushSync(() => { navigate(to, options); }); }); }; const App = () => ( {/* Your routes here */} ); ``` You can also enable transitions selectively using the `transition` prop, which will be available in the `options` parameter: ```jsx // Enable transition for a specific link About // Or programmatically const [location, navigate] = useLocation(); navigate("/about", { transition: true }); // Then check for it in your handler const aroundNav: AroundNavHandler = (navigate, to, options) => { if (!document.startViewTransition) { navigate(to, options); return; } if (options?.transition) { document.startViewTransition(() => { flushSync(() => { navigate(to, options); }); }); } else { navigate(to, options); } }; ``` ### Preact support? Preact exports are available through a separate package named `wouter-preact` (or within the `wouter/preact` namespace, however this method isn't recommended as it requires React as a peer dependency): ```diff - import { useRoute, Route, Switch } from "wouter"; + import { useRoute, Route, Switch } from "wouter-preact"; ``` You might need to ensure you have the latest version of [Preact X](https://github.com/preactjs/preact/releases/tag/10.0.0-alpha.0) with support for hooks. **[▶ Demo Sandbox](https://codesandbox.io/s/wouter-preact-0lr3n)** ### Server-side Rendering support (SSR)? In order to render your app on the server, you'll need to wrap your app with top-level Router and specify `ssrPath` prop (usually, derived from current request). Optionally, `Router` accepts `ssrSearch` parameter if need to have access to a search string on a server. ```js import { renderToString } from "react-dom/server"; import { Router } from "wouter"; const handleRequest = (req, res) => { // top-level Router is mandatory in SSR mode // pass an optional context object to handle redirects on the server const ssrContext = {}; const prerendered = renderToString( ); if (ssrContext.redirectTo) { // encountered redirect res.redirect(ssrContext.redirectTo); } else { // respond with prerendered html } }; ``` Tip: wouter can pre-fill `ssrSearch`, if `ssrPath` contains the `?` character. So these are equivalent: ```jsx ; // is the same as ; ``` On the client, the static markup must be hydrated in order for your app to become interactive. Note that to avoid having hydration warnings, the JSX rendered on the client must match the one used by the server, so the `Router` component must be present. ```js import { hydrateRoot } from "react-dom/client"; const root = hydrateRoot( domNode, // during hydration, `ssrPath` is set to `location.pathname`, // `ssrSearch` set to `location.search` accordingly // so there is no need to explicitly specify them ); ``` **[▶ Demo](https://github.com/molefrog/wultra)** ### How do I configure the router to render a specific route in tests? Testing with wouter is no different from testing regular React apps. You often need a way to provide a fixture for the current location to render a specific route. This can be easily done by swapping the normal location hook with `memoryLocation`. It is an initializer function that returns a hook that you can then specify in a top-level `Router`. ```jsx import { render } from "@testing-library/react"; import { memoryLocation } from "wouter/memory-location"; it("renders a user page", () => { // `static` option makes it immutable // even if you call `navigate` somewhere in the app location won't change const { hook, searchHook } = memoryLocation({ path: "/user/2", static: true }); const { container } = render( {(params) => <>User ID: {params.id}} ); expect(container.innerHTML).toBe("User ID: 2"); }); ``` **Note:** When you pass a `hook` prop to `Router`, it will automatically inherit the `searchHook` from the hook if available (via `hook.searchHook`). This means you don't need to explicitly pass both `hook` and `searchHook` when using `memoryLocation` - just passing `hook` is enough for `useSearch()` to work correctly with query parameters. ```jsx it("works with query parameters", () => { const { hook } = memoryLocation({ path: "/products?sort=price&order=asc" }); const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => {children}, }); expect(result.current).toBe("sort=price&order=asc"); }); ``` The hook can be configured to record navigation history. Additionally, it comes with a `navigate` function for external navigation. ```jsx it("performs a redirect", () => { const { hook, history, navigate } = memoryLocation({ path: "/", // will store navigation history in `history` record: true, }); const { container } = render( Index Orders ); expect(history).toStrictEqual(["/"]); navigate("/unknown/route"); expect(container.innerHTML).toBe("Orders"); expect(history).toStrictEqual(["/", "/unknown/route", "/orders"]); }); ``` ### 1KB is too much, I can't afford it! We've got some great news for you! If you're a minimalist bundle-size nomad and you need a damn simple routing in your app, you can just use bare location hooks. For example, `useBrowserLocation` hook which is only **650 bytes gzipped** and manually match the current location with it: ```js import { useBrowserLocation } from "wouter/use-browser-location"; const UsersRoute = () => { const [location] = useBrowserLocation(); if (location !== "/users") return null; // render the route }; ``` Wouter's motto is **"Minimalist-friendly"**. ## Contributing **Architecture principles:** - All code is written in JavaScript for full control over size optimization - TypeScript definitions are maintained separately in `types/` directories - `wouter-preact` reuses the same source except for `react-deps.js` (Preact-specific hooks) - Type definitions are duplicated between packages (not ideal, but works for now) **Development:** Tests run directly from source files (no build required). Run `npm run test` for interactive mode or `npm run test -- --run` for a single run. Use `npm run build` to build the distributable package before publishing. ## Acknowledgements Wouter illustrations and logos were made by [Katya Simacheva](https://simachevakatya.com/) and [Katya Vakulenko](https://katyavakulenko.com/). Thank you to **[@jeetiss](https://github.com/jeetiss)** and all the amazing [contributors](https://github.com/molefrog/wouter/graphs/contributors) for helping with the development. ================================================ FILE: bunfig.toml ================================================ [test] preload = ["./packages/wouter/test/setup.ts"] coverageSkipTestFiles = true coveragePathIgnorePatterns = ["**/test/**"] ================================================ FILE: package.json ================================================ { "name": "monorepo", "private": true, "description": "A minimalistic routing for React and Preact. Monorepo package.", "type": "module", "workspaces": [ "packages/*" ], "scripts": { "fix:p": "prettier --write \"./**/*.(js|ts){x,}\"", "test": "bun test", "test-types": "tsc --noEmit", "size": "size-limit", "lint": "eslint packages/**/*.js", "build": ":" }, "author": "Alexey Taktarov ", "repository": { "type": "git", "url": "git+https://github.com/molefrog/wouter.git" }, "license": "ISC", "sideEffects": false, "prettier": { "tabWidth": 2, "semi": true, "singleQuote": false, "printWidth": 80 }, "size-limit": [ { "path": "packages/wouter/src/index.js", "limit": "2500 B", "ignore": [ "react", "use-sync-external-store" ] }, { "path": "packages/wouter/src/use-browser-location.js", "limit": "1000 B", "import": "{ useBrowserLocation }", "ignore": [ "react", "use-sync-external-store" ] }, { "path": "packages/wouter/src/memory-location.js", "limit": "1000 B", "ignore": [ "react", "use-sync-external-store" ] }, { "path": "packages/wouter/src/use-hash-location.js", "limit": "1000 B", "ignore": [ "react", "use-sync-external-store" ] }, { "path": "packages/wouter-preact/src/index.js", "limit": "2500 B", "ignore": [ "preact", "preact/hooks" ] }, { "path": "packages/wouter-preact/src/use-browser-location.js", "limit": "1000 B", "import": "{ useBrowserLocation }", "ignore": [ "preact", "preact/hooks" ] }, { "path": "packages/wouter-preact/src/use-hash-location.js", "limit": "1000 B", "ignore": [ "preact", "preact/hooks" ] }, { "path": "packages/wouter-preact/src/memory-location.js", "limit": "1000 B", "ignore": [ "preact", "preact/hooks" ] } ], "husky": { "hooks": { "commit-msg": "npm run fix:p" } }, "eslintConfig": { "extends": "eslint:recommended", "parserOptions": { "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "env": { "es2020": true, "browser": true, "node": true }, "rules": { "no-unused-vars": [ "error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" } ], "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" }, "plugins": [ "react-hooks" ], "ignorePatterns": [ "types/**" ] }, "devDependencies": { "@happy-dom/global-registrator": "^20.0.10", "@size-limit/preset-small-lib": "^11.2.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^16.3.0", "@types/bun": "1.3.3", "@types/react": "^19", "@types/react-dom": "^19", "copyfiles": "^2.4.1", "eslint": "^7.19.0", "eslint-plugin-react-hooks": "^4.6.2", "happy-dom": "^20.0.10", "husky": "^4.3.0", "path-to-regexp": "^6.2.1", "preact": "^10.23.2", "preact-render-to-string": "^6.5.9", "prettier": "^2.4.1", "react": "^19", "react-dom": "^19", "size-limit": "^11.2.0", "typescript": "^5.8.0" } } ================================================ FILE: packages/magazin/.gitignore ================================================ # dependencies (bun install) node_modules # output out dist *.tgz # code coverage coverage *.lcov # logs logs _.log report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # caches .eslintcache .cache *.tsbuildinfo # IntelliJ based IDEs .idea # Finder (MacOS) folder config .DS_Store ================================================ FILE: packages/magazin/App.tsx ================================================ import { Route, Switch, Redirect } from "wouter"; import { Helmet } from "@dr.pogodin/react-helmet"; import { HomePage } from "@/routes/home.tsx"; import { AboutPage } from "@/routes/about.tsx"; import { NotFoundPage } from "@/routes/404.tsx"; import { ProductPage } from "@/routes/products/[slug].tsx"; import { CartPage } from "@/routes/cart.tsx"; import { WithStatusCode } from "@/components/with-status-code.tsx"; import { Navbar } from "@/components/navbar.tsx"; import { Footer } from "@/components/footer.tsx"; export function App() { return (
{(params) => }
); } ================================================ FILE: packages/magazin/client.tsx ================================================ import { hydrateRoot } from "react-dom/client"; import { flushSync } from "react-dom"; import { Router, type AroundNavHandler } from "wouter"; import { HelmetProvider } from "@dr.pogodin/react-helmet"; import { App } from "./App"; // Enable view transitions for navigation const aroundNav: AroundNavHandler = (navigate, to, options) => { // Feature detection for browsers that don't support View Transitions if (!document.startViewTransition) { navigate(to, options); return; } // Only use view transitions if explicitly requested if (options?.transition) { document.startViewTransition(() => { flushSync(() => { navigate(to, options); }); }); } else { navigate(to, options); } }; hydrateRoot( document.body, ); ================================================ FILE: packages/magazin/components/footer.tsx ================================================ import { Link } from "wouter"; export function Footer() { return (
magazin

A modern e-commerce demo built with wouter and Bun.
No rights reserved.

); } ================================================ FILE: packages/magazin/components/navbar.tsx ================================================ import { Link } from "wouter"; function Logo() { return ; } function NavLink({ href, children, }: { href: string; children: React.ReactNode; }) { return ( `text-sm font-medium ${ active ? "text-gray-900" : "text-gray-500 hover:text-gray-900" }` } > {children} ); } export function Navbar() { return ( ); } ================================================ FILE: packages/magazin/components/star-wouter.tsx ================================================ import { useState, useEffect } from "react"; export function StarWouter() { const [stars, setStars] = useState(null); useEffect(() => { fetch("https://api.github.com/repos/molefrog/wouter") .then((res) => res.json()) .then((data) => { if (data.stargazers_count) { setStars(data.stargazers_count); } }) .catch(() => { // If fetch fails, we just won't show the count setStars(null); }); }, []); return ( Star Wouter
{stars !== null ? stars.toLocaleString() : "\u221E"}
); } ================================================ FILE: packages/magazin/components/with-status-code.tsx ================================================ import { useRouter } from "wouter"; export function WithStatusCode({ code, children, }: { code: number; children: React.ReactNode; }) { const router = useRouter(); // Set status code on SSR context if available if (router.ssrContext) { router.ssrContext.statusCode = code; } return <>{children}; } ================================================ FILE: packages/magazin/db/products.ts ================================================ export interface Product { slug: string; name: string; price: number; brand: string; category: string; image: string; description: string; } export const products: Product[] = [ { image: "/products/carabiner.webp", slug: "hook-keyring-rvst", brand: "RVST", category: "Accessories", name: "Hook Keyring", price: 65, description: "Premium carabiner keyring crafted with attention to detail and designed for everyday carry.", }, { image: "/products/ring.webp", slug: "silver-ok-ring", brand: "Rick Woens", category: "Jewelry", name: "Silver OK Ring", price: 99, description: "Handcrafted sterling silver ring with a unique OK gesture design.", }, { image: "/products/navigator-cap.webp", slug: "navigator-baseball-cap", brand: "Rendr", category: "Accessories", name: "Navigator Baseball Cap", price: 179, description: "Premium baseball cap with embroidered branding and adjustable fit.", }, { image: "/products/sizelimited-tshirt.webp", slug: "size-limited-tshirt", brand: "Rendr", category: "Clothing", name: "Size Limited T-Shirt", price: 65, description: "Comfortable cotton t-shirt with minimalist branding and premium fit.", }, { image: "/products/wouter-glasses.webp", slug: "wouter-cult-glasses", brand: "Wouter", category: "Accessories", name: "Wouter Glasses", price: 129, description: "Cult glasses worn by wouter. Minimalist design with premium frames and crystal-clear lenses.", }, { image: "/products/parka.webp", slug: "route-breaker-windbreaker", brand: "Wouter", category: "Clothing", name: "Route Breaker Windbreaker", price: 249, description: "Navigate any weather with the Route Breaker. Lightweight, water-resistant, and built for those who hook into adventure.", }, { image: "/products/react-pendant.webp", slug: "react-state-pendant", brand: "Wouter", category: "Jewelry", name: "React State Pendant", price: 159, description: "A declarative pendant for those who embrace the component lifecycle. Hooks perfectly with any chain.", }, { image: "/products/scarf.webp", slug: "nested-routes-silk-scarf", brand: "Wouter", category: "Accessories", name: "Nested Routes Silk Scarf", price: 189, description: "Luxurious silk scarf featuring an intricate wouter pattern. Each layer wraps seamlessly into the next, just like your favorite routes.", }, { image: "/products/poster-a.webp", slug: "keep-routing-poster", brand: "Wouter", category: "Art", name: "Keep Routing Poster", price: 45, description: "Minimalist poster with a bold message for developers. Museum-quality print that reminds you to stay on the path.", }, ]; export function getProductBySlug(slug: string): Product | undefined { return products.find((p) => p.slug === slug); } ================================================ FILE: packages/magazin/index.html ================================================ <!-- injected during SSR --> ================================================ FILE: packages/magazin/index.tsx ================================================ import { renderToReadableStream } from "react-dom/server"; import { Router } from "wouter"; import { HelmetProvider, type HelmetDataContext, } from "@dr.pogodin/react-helmet"; import { App } from "./App.tsx"; import tailwind from "bun-plugin-tailwind"; // Build the HTML and all its assets before starting the server const isProduction = process.env.NODE_ENV === "production"; const build = await Bun.build({ entrypoints: ["./index.html"], // No outdir = files are kept in memory, not written to disk minify: isProduction, publicPath: "/", sourcemap: "linked", plugins: [tailwind], define: { "process.env.NODE_ENV": JSON.stringify( process.env.NODE_ENV || "development" ), }, }); if (!build.success) { console.error("Build failed:", build.logs); process.exit(1); } // Create a map of assets by their path for quick lookup const assets = new Map(); let htmlTemplate: string | null = null; for (const output of build.outputs) { // The HTML file will be used as template for SSR if (output.path.endsWith(".html")) { htmlTemplate = await output.text(); } else { // Store other assets (JS, CSS, etc.) by their basename const basename = "/" + output.path.split("/").pop()!; assets.set(basename, output); } } if (!htmlTemplate) { console.error("No HTML template found in build outputs"); process.exit(1); } const port = process.env.PORT ? parseInt(process.env.PORT) : 3002; Bun.serve({ port, async fetch(req) { const url = new URL(req.url); // Check if this is a request for a built asset const asset = assets.get(url.pathname); if (asset) { const headers = isProduction ? { // Built assets have content hashes, so they can be cached indefinitely "Cache-Control": "public, max-age=31536000, immutable", } : {}; return new Response(asset, { headers }); } // Check if this is a request for a static file from public/ const publicFile = Bun.file(`./public${url.pathname}`); if (await publicFile.exists()) { const headers = new Headers(); // Add 24h caching for static assets in production if (isProduction) { headers.set("Cache-Control", "public, max-age=86400"); } return new Response(publicFile, { headers }); } // Otherwise, it's a page request - render with SSR // ssrPath accepts full path with search, e.g. "/foo?bar=1" // ssrContext is used to handle redirects and status codes on the server const ssrContext: { redirectTo?: string; statusCode?: number } = {}; const helmetContext: HelmetDataContext = {}; const stream = await renderToReadableStream( ); // Check if a redirect occurred during SSR if (ssrContext.redirectTo) { return Response.redirect( new URL(ssrContext.redirectTo, url.origin).toString(), 302 ); } // Get status code from context, default to 200 const statusCode = ssrContext.statusCode || 200; // Convert stream to string const appHtml = await new Response(stream).text(); const helmet = helmetContext.helmet; // Use HTMLRewriter to inject the SSR content into body and title const rewriter = new HTMLRewriter() .on("body", { element(element) { element.setInnerContent(appHtml, { html: true }); }, }) .on("title", { element(element) { if (!helmet) return; // Remove the existing title tag and let helmet's title be appended to head element.remove(); }, }) .on("head", { element(element) { if (!helmet) return; const headContent = [ helmet.title?.toString(), helmet.priority?.toString(), helmet.meta?.toString(), helmet.link?.toString(), helmet.script?.toString(), ] .filter(Boolean) .join("\n"); if (headContent) { element.append(headContent, { html: true }); } }, }); const transformedResponse = rewriter.transform(new Response(htmlTemplate)); return new Response(transformedResponse.body, { status: statusCode, headers: { "Content-Type": "text/html" }, }); }, }); console.log(`Server running at http://localhost:${port}`); ================================================ FILE: packages/magazin/package.json ================================================ { "name": "magazin", "module": "index.ts", "type": "module", "private": true, "scripts": { "dev": "bun --watch index.tsx", "start": "bun index.tsx", "prod": "NODE_ENV=production bun index.tsx" }, "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5" }, "dependencies": { "@dr.pogodin/react-helmet": "^3.0.4", "bun-plugin-tailwind": "^0.1.2", "tailwindcss": "^4.1.17", "wouter": "workspace:*" } } ================================================ FILE: packages/magazin/routes/404.tsx ================================================ import { Link } from "wouter"; import { Helmet } from "@dr.pogodin/react-helmet"; export function NotFoundPage() { return (
Page Not Found
404

Not Found

We are sorry, but the page you're looking for doesn't exist. Try going back to the{" "} home page .

); } ================================================ FILE: packages/magazin/routes/about.tsx ================================================ import { Link } from "wouter"; import { Helmet } from "@dr.pogodin/react-helmet"; function Feature({ children }: { children: React.ReactNode }) { return (
  • {children}
  • ); } export function AboutPage() { return ( <> About

    What is this?

    This is a simple SSR demo showcasing wouter v3.9.0, React 19 with server-side rendering and client-side hydration running on Bun.

    Features
      Dynamic segments Default switch route (404) Search parameters Redirect with SSR support Active links Navigation with state Custom status codes (404) View transitions
    ); } ================================================ FILE: packages/magazin/routes/cart.tsx ================================================ import { useEffect, useState } from "react"; import { useLocation } from "wouter"; import { Helmet } from "@dr.pogodin/react-helmet"; import { products, type Product } from "@/db/products"; const cartItems: Array<{ product: Product; quantity: number }> = [ { product: products[4]!, quantity: 1 }, // Wouter Glasses { product: products[5]!, quantity: 1 }, // Route Breaker Windbreaker { product: products[0]!, quantity: 2 }, // Hook Keyring { product: products[7]!, quantity: 3 }, // Keep Routing Poster ]; function NotificationBanner({ show, message, }: { show: boolean; message: string | null; }) { if (!message) return null; return (
    {message} added to cart
    ); } export function CartPage() { const [location, navigate] = useLocation(); const [showNotification, setShowNotification] = useState(false); const [addedItem, setAddedItem] = useState(null); useEffect(() => { const state = history.state as { addedItem?: string } | null; if (state?.addedItem) { setAddedItem(state.addedItem); setShowNotification(true); // Clear the state so it doesn't show again on refresh navigate(location, { replace: true, state: null }); // Hide notification after 3 seconds const timer = setTimeout(() => { setShowNotification(false); }, 3000); return () => clearTimeout(timer); } }, [location, navigate]); return ( <> Cart

    Shopping Cart

    {cartItems.map((item, index) => (
    {item.product.name}
    {item.product.name} {item.quantity} × ${item.product.price}
    ${item.product.price}
    ))}
    Total
    $643
    ); } ================================================ FILE: packages/magazin/routes/home.tsx ================================================ import { useSearchParams, Link } from "wouter"; import { Helmet } from "@dr.pogodin/react-helmet"; import { products, type Product } from "@/db/products"; import { StarWouter } from "@/components/star-wouter"; function ProductCard({ slug, brand, category, name, price, image }: Product) { return (
    {name}
    {brand} · {category}
    {name} ${price.toLocaleString()}
    ); } const categories = [ { value: "all", label: "All" }, { value: "accessories", label: "Accessories" }, { value: "clothing", label: "Clothing" }, { value: "jewelry", label: "Jewelry" }, { value: "art", label: "Art" }, ]; const sortOptions = [ { value: "newest", label: "Newest" }, { value: "price-asc", label: "Price: Low to High" }, { value: "price-desc", label: "Price: High to Low" }, { value: "name", label: "Name" }, ]; function CategoryFilter({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { return (
    {categories.map((cat) => ( ))}
    ); } function SortSelect({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { return (
    ); } export function HomePage() { const [searchParams, setSearchParams] = useSearchParams(); const category = searchParams.get("category") || "all"; const sort = searchParams.get("sort") || "newest"; const handleFilterChange = (key: string, value: string) => { setSearchParams((params) => { const newParams = new URLSearchParams(params); if (value === "all" || value === "newest") { newParams.delete(key); } else { newParams.set(key, value); } return newParams; }); }; // Filter products by category let filteredProducts = products; if (category !== "all") { filteredProducts = products.filter( (p) => p.category.toLowerCase() === category.toLowerCase() ); } // Sort products const sortedProducts = [...filteredProducts].sort((a, b) => { switch (sort) { case "price-asc": return a.price - b.price; case "price-desc": return b.price - a.price; case "name": return a.name.localeCompare(b.name); case "newest": default: return 0; // Keep original order } }); return ( <> Magazin by wouter

    Welcome to our shop

    Exclusive merch for hardcore wouter fans. You can't buy these yet, so go star the repo to increase our chances of becoming a billion dollar company.

    handleFilterChange("category", v)} /> handleFilterChange("sort", v)} />
    {sortedProducts.map((product) => ( ))}
    {sortedProducts.length === 0 && (
    No products found in this category.
    )} ); } ================================================ FILE: packages/magazin/routes/products/[slug].tsx ================================================ import { Link } from "wouter"; import { Helmet } from "@dr.pogodin/react-helmet"; import { getProductBySlug } from "@/db/products"; export function ProductPage({ slug }: { slug: string }) { const product = getProductBySlug(slug); if (!product) { return (
    Product Not Found

    Product not found

    Back to home
    ); } return ( <> {product.name}
    {product.name}
    {product.brand} · {product.category}

    {product.name}

    {product.description}

    ${product.price}
    Add to Cart
    ); } ================================================ FILE: packages/magazin/styles.css ================================================ @import "tailwindcss"; /* View Transitions */ @view-transition { navigation: auto; } /* Default: simple 0.25s cross-fade */ ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.2s; animation-timing-function: ease; } ================================================ FILE: packages/magazin/tsconfig.json ================================================ { "compilerOptions": { // Environment setup & latest features "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, // Path aliases "baseUrl": ".", "paths": { "@/*": ["./*"] }, // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false } } ================================================ FILE: packages/wouter/package.json ================================================ { "name": "wouter", "version": "3.9.0", "description": "Minimalist-friendly ~1.5KB router for React", "type": "module", "keywords": [ "react", "preact", "router", "tiny", "routing", "hooks", "useLocation" ], "files": [ "src", "types/**/*.d.ts", "types/*.d.ts" ], "main": "src/index.js", "exports": { ".": { "types": "./types/index.d.ts", "default": "./src/index.js" }, "./use-browser-location": { "types": "./types/use-browser-location.d.ts", "default": "./src/use-browser-location.js" }, "./use-hash-location": { "types": "./types/use-hash-location.d.ts", "default": "./src/use-hash-location.js" }, "./memory-location": { "types": "./types/memory-location.d.ts", "default": "./src/memory-location.js" } }, "types": "types/index.d.ts", "typesVersions": { ">=4.1": { "types/index.d.ts": [ "types/index.d.ts" ], "use-browser-location": [ "types/use-browser-location.d.ts" ], "use-hash-location": [ "types/use-hash-location.d.ts" ], "memory-location": [ "types/memory-location.d.ts" ] } }, "scripts": { "prepublishOnly": "cp ../../README.md ." }, "author": "Alexey Taktarov ", "repository": { "type": "git", "url": "git+https://github.com/molefrog/wouter.git" }, "license": "Unlicense", "peerDependencies": { "react": ">=16.8.0" }, "dependencies": { "mitt": "^3.0.1", "regexparam": "^3.0.0", "use-sync-external-store": "^1.0.0" } } ================================================ FILE: packages/wouter/src/index.d.ts ================================================ export * from "../types/index.js"; ================================================ FILE: packages/wouter/src/index.js ================================================ import { parse as parsePattern } from "regexparam"; import { useBrowserLocation, useSearch as useBrowserSearch, } from "./use-browser-location.js"; import { useRef, useContext, createContext, isValidElement, cloneElement, createElement as h, Fragment, forwardRef, useIsomorphicLayoutEffect, useEvent, useMemo, } from "./react-deps.js"; import { absolutePath, relativePath, sanitizeSearch } from "./paths.js"; /* * Router and router context. Router is a lightweight object that represents the current * routing options: how location is managed, base path etc. * * There is a default router present for most of the use cases, however it can be overridden * via the component. */ const defaultRouter = { hook: useBrowserLocation, searchHook: useBrowserSearch, parser: parsePattern, base: "", // this option is used to override the current location during SSR ssrPath: undefined, ssrSearch: undefined, // optional context to track render state during SSR ssrContext: undefined, // customizes how `href` props are transformed for hrefs: (x) => x, // wraps navigate calls, useful for view transitions aroundNav: (n, t, o) => n(t, o), }; const RouterCtx = createContext(defaultRouter); // gets the closest parent router from the context export const useRouter = () => useContext(RouterCtx); /** * Parameters context. Used by `useParams()` to get the * matched params from the innermost `Route` component. */ const Params0 = {}, ParamsCtx = createContext(Params0); export const useParams = () => useContext(ParamsCtx); /* * Part 1, Hooks API: useRoute and useLocation */ // Internal version of useLocation to avoid redundant useRouter calls const useLocationFromRouter = (router) => { const [location, navigate] = router.hook(router); // the function reference should stay the same between re-renders, so that // it can be passed down as an element prop without any performance concerns. // (This is achieved via `useEvent`.) return [ relativePath(router.base, location), useEvent((to, opts) => router.aroundNav(navigate, absolutePath(to, router.base), opts) ), ]; }; export const useLocation = () => useLocationFromRouter(useRouter()); export const useSearch = () => { const router = useRouter(); return sanitizeSearch(router.searchHook(router)); }; export const matchRoute = (parser, route, path, loose) => { // if the input is a regexp, skip parsing const { pattern, keys } = route instanceof RegExp ? { keys: false, pattern: route } : parser(route || "*", loose); // array destructuring loses keys, so this is done in two steps const result = pattern.exec(path) || []; // when parser is in "loose" mode, `$base` is equal to the // first part of the route that matches the pattern // (e.g. for pattern `/a/:b` and path `/a/1/2/3` the `$base` is `a/1`) // we use this for route nesting const [$base, ...matches] = result; return $base !== undefined ? [ true, (() => { // for regex paths, `keys` will always be false // an object with parameters matched, e.g. { foo: "bar" } for "/:foo" // we "zip" two arrays here to construct the object // ["foo"], ["bar"] → { foo: "bar" } const groups = keys !== false ? Object.fromEntries(keys.map((key, i) => [key, matches[i]])) : result.groups; // convert the array to an instance of object // this makes it easier to integrate with the existing param implementation let obj = { ...matches }; // merge named capture groups with matches array groups && Object.assign(obj, groups); return obj; })(), // the third value if only present when parser is in "loose" mode, // so that we can extract the base path for nested routes ...(loose ? [$base] : []), ] : [false, null]; }; export const useRoute = (pattern) => matchRoute(useRouter().parser, pattern, useLocation()[0]); /* * Part 2, Low Carb Router API: Router, Route, Link, Switch */ export const Router = ({ children, ...props }) => { // the router we will inherit from - it is the closest router in the tree, // unless the custom `hook` is provided (in that case it's the default one) const parent_ = useRouter(); const parent = props.hook ? defaultRouter : parent_; // holds to the context value: the router object let value = parent; // when `ssrPath` contains a `?` character, we can extract the search from it. // also, ensure ssrSearch is always defined when ssrPath is provided, so that // useSearch behavior matches usePathname (proper SSR hydration when client // renders without props after server rendered with ssrPath/ssrSearch) const [path, search = props.ssrSearch ?? ""] = props.ssrPath?.split("?") ?? []; if (path) (props.ssrSearch = search), (props.ssrPath = path); // hooks can define their own `href` formatter (e.g. for hash location) props.hrefs = props.hrefs ?? props.hook?.hrefs; // hooks can define their own search hook (e.g. for memory location) props.searchHook = props.searchHook ?? props.hook?.searchHook; // what is happening below: to avoid unnecessary rerenders in child components, // we ensure that the router object reference is stable, unless there are any // changes that require reload (e.g. `base` prop changes -> all components that // get the router from the context should rerender, even if the component is memoized). // the expected behaviour is: // // 1) when the resulted router is no different from the parent, use parent // 2) if the custom `hook` prop is provided, we always inherit from the // default router instead. this resets all previously overridden options. // 3) when the router is customized here, it should stay stable between renders let ref = useRef({}), prev = ref.current, next = prev; for (let k in parent) { const option = k === "base" ? /* base is special case, it is appended to the parent's base */ parent[k] + (props[k] ?? "") : props[k] ?? parent[k]; if (prev === next && option !== next[k]) { ref.current = next = { ...next }; } next[k] = option; // the new router is no different from the parent or from the memoized value, use parent if (option !== parent[k] || option !== value[k]) value = next; } return h(RouterCtx.Provider, { value, children }); }; const h_route = ({ children, component }, params) => { // React-Router style `component` prop if (component) return h(component, { params }); // support render prop or plain children return typeof children === "function" ? children(params) : children; }; // Cache params object between renders if values are shallow equal const useCachedParams = (value) => { let prev = useRef(Params0); const curr = prev.current; return (prev.current = // Update cache if number of params changed or any value changed Object.keys(value).length !== Object.keys(curr).length || Object.entries(value).some(([k, v]) => v !== curr[k]) ? value // Return new value if there are changes : curr); // Return cached value if nothing changed }; export function useSearchParams() { const [location, navigate] = useLocation(); const search = useSearch(); const searchParams = useMemo(() => new URLSearchParams(search), [search]); // cached value before next render, so you can call setSearchParams multiple times let tempSearchParams = searchParams; const setSearchParams = useEvent((nextInit, options) => { tempSearchParams = new URLSearchParams( typeof nextInit === "function" ? nextInit(tempSearchParams) : nextInit ); navigate(location + "?" + tempSearchParams, options); }); return [searchParams, setSearchParams]; } export const Route = ({ path, nest, match, ...renderProps }) => { const router = useRouter(); const [location] = useLocationFromRouter(router); const [matches, routeParams, base] = // `match` is a special prop to give up control to the parent, // it is used by the `Switch` to avoid double matching match ?? matchRoute(router.parser, path, location, nest); // when `routeParams` is `null` (there was no match), the argument // below becomes {...null} = {}, see the Object Spread specs // https://tc39.es/proposal-object-rest-spread/#AbstractOperations-CopyDataProperties const params = useCachedParams({ ...useParams(), ...routeParams }); if (!matches) return null; const children = base ? h(Router, { base }, h_route(renderProps, params)) : h_route(renderProps, params); return h(ParamsCtx.Provider, { value: params, children }); }; export const Link = forwardRef((props, ref) => { const router = useRouter(); const [currentPath, navigate] = useLocationFromRouter(router); const { to = "", href: targetPath = to, onClick: _onClick, asChild, children, className: cls, /* eslint-disable no-unused-vars */ replace /* ignore nav props */, state /* ignore nav props */, transition /* ignore nav props */, /* eslint-enable no-unused-vars */ ...restProps } = props; const onClick = useEvent((event) => { // ignores the navigation when clicked using right mouse button or // by holding a special modifier key: ctrl, command, win, alt, shift if ( event.ctrlKey || event.metaKey || event.altKey || event.shiftKey || event.button !== 0 ) return; _onClick?.(event); if (!event.defaultPrevented) { event.preventDefault(); navigate(targetPath, props); } }); // handle nested routers and absolute paths const href = router.hrefs( targetPath[0] === "~" ? targetPath.slice(1) : router.base + targetPath, router // pass router as a second argument for convinience ); return asChild && isValidElement(children) ? cloneElement(children, { onClick, href }) : h("a", { ...restProps, onClick, href, // `className` can be a function to apply the class if this link is active className: cls?.call ? cls(currentPath === targetPath) : cls, children, ref, }); }); const flattenChildren = (children) => Array.isArray(children) ? children.flatMap((c) => flattenChildren(c && c.type === Fragment ? c.props.children : c) ) : [children]; export const Switch = ({ children, location }) => { const router = useRouter(); const [originalLocation] = useLocationFromRouter(router); for (const element of flattenChildren(children)) { let match = 0; if ( isValidElement(element) && // we don't require an element to be of type Route, // but we do require it to contain a truthy `path` prop. // this allows to use different components that wrap Route // inside of a switch, for example . (match = matchRoute( router.parser, element.props.path, location || originalLocation, element.props.nest ))[0] ) return cloneElement(element, { match }); } return null; }; export const Redirect = (props) => { const { to, href = to } = props; const router = useRouter(); const [, navigate] = useLocationFromRouter(router); const redirect = useEvent(() => navigate(to || href, props)); const { ssrContext } = router; // redirect is guaranteed to be stable since it is returned from useEvent useIsomorphicLayoutEffect(() => { redirect(); }, []); // eslint-disable-line react-hooks/exhaustive-deps if (ssrContext) { ssrContext.redirectTo = to; } return null; }; ================================================ FILE: packages/wouter/src/memory-location.d.ts ================================================ export * from "../types/memory-location.js"; ================================================ FILE: packages/wouter/src/memory-location.js ================================================ import mitt from "mitt"; import { useSyncExternalStore } from "./react-deps.js"; /** * In-memory location that supports navigation */ export const memoryLocation = ({ path = "/", searchPath = "", static: staticLocation, record, } = {}) => { let initialPath = path; if (searchPath) { // join with & if path contains search query, and ? otherwise initialPath += path.split("?")[1] ? "&" : "?"; initialPath += searchPath; } let [currentPath, currentSearch = ""] = initialPath.split("?"); const history = [initialPath]; const emitter = mitt(); const navigateImplementation = (path, { replace = false } = {}) => { if (record) { if (replace) { history.splice(history.length - 1, 1, path); } else { history.push(path); } } [currentPath, currentSearch = ""] = path.split("?"); emitter.emit("navigate", path); }; const navigate = !staticLocation ? navigateImplementation : () => null; const subscribe = (cb) => { emitter.on("navigate", cb); return () => emitter.off("navigate", cb); }; const useMemoryLocation = () => [ useSyncExternalStore(subscribe, () => currentPath), navigate, ]; const useMemoryQuery = () => useSyncExternalStore(subscribe, () => currentSearch); // Attach searchHook to the location hook for auto-inheritance in Router useMemoryLocation.searchHook = useMemoryQuery; function reset() { // clean history array with mutation to preserve link history.splice(0, history.length); navigateImplementation(initialPath); } return { hook: useMemoryLocation, searchHook: useMemoryQuery, navigate, history: record ? history : undefined, reset: record ? reset : undefined, }; }; ================================================ FILE: packages/wouter/src/paths.js ================================================ /* * Transforms `path` into its relative `base` version * If base isn't part of the path provided returns absolute path e.g. `~/app` */ const _relativePath = (base, path) => !path.toLowerCase().indexOf(base.toLowerCase()) ? path.slice(base.length) || "/" : "~" + path; /** * When basepath is `undefined` or '/' it is ignored (we assume it's empty string) */ const baseDefaults = (base = "") => (base === "/" ? "" : base); export const absolutePath = (to, base) => to[0] === "~" ? to.slice(1) : baseDefaults(base) + to; export const relativePath = (base = "", path) => _relativePath(unescape(baseDefaults(base)), unescape(path)); /* * Removes leading question mark */ const stripQm = (str) => (str[0] === "?" ? str.slice(1) : str); /* * decodes escape sequences such as %20 */ const unescape = (str) => { try { return decodeURI(str); } catch (_e) { // fail-safe mode: if string can't be decoded do nothing return str; } }; export const sanitizeSearch = (search) => unescape(stripQm(search)); ================================================ FILE: packages/wouter/src/react-deps.js ================================================ import * as React from "react"; // React.useInsertionEffect is not available in React <18 // This hack fixes a transpilation issue on some apps const useBuiltinInsertionEffect = React["useInsertion" + "Effect"]; export { useMemo, useRef, useState, useContext, createContext, isValidElement, cloneElement, createElement, Fragment, forwardRef, } from "react"; // To resolve webpack 5 errors, while not presenting problems for native, // we copy the approaches from https://github.com/TanStack/query/pull/3561 // and https://github.com/TanStack/query/pull/3601 // ~ Show this aging PR some love to remove the need for this hack: // https://github.com/facebook/react/pull/25231 ~ export { useSyncExternalStore } from "./use-sync-external-store.js"; // Copied from: // https://github.com/facebook/react/blob/main/packages/shared/ExecutionEnvironment.js const canUseDOM = !!( typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined" ); // Copied from: // https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.ts // "React currently throws a warning when using useLayoutEffect on the server. // To get around it, we can conditionally useEffect on the server (no-op) and // useLayoutEffect in the browser." export const useIsomorphicLayoutEffect = canUseDOM ? React.useLayoutEffect : React.useEffect; // useInsertionEffect is already a noop on the server. // See: https://github.com/facebook/react/blob/main/packages/react-server/src/ReactFizzHooks.js export const useInsertionEffect = useBuiltinInsertionEffect || useIsomorphicLayoutEffect; // Userland polyfill while we wait for the forthcoming // https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md // Note: "A high-fidelity polyfill for useEvent is not possible because // there is no lifecycle or Hook in React that we can use to switch // .current at the right timing." // So we will have to make do with this "close enough" approach for now. export const useEvent = (fn) => { const ref = React.useRef([fn, (...args) => ref[0](...args)]).current; // Per Dan Abramov: useInsertionEffect executes marginally closer to the // correct timing for ref synchronization than useLayoutEffect on React 18. // See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360 useInsertionEffect(() => { ref[0] = fn; }); return ref[1]; }; ================================================ FILE: packages/wouter/src/use-browser-location.d.ts ================================================ export * from "../types/use-browser-location.js"; ================================================ FILE: packages/wouter/src/use-browser-location.js ================================================ import { useSyncExternalStore } from "./react-deps.js"; /** * History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History */ const eventPopstate = "popstate"; const eventPushState = "pushState"; const eventReplaceState = "replaceState"; const eventHashchange = "hashchange"; const events = [ eventPopstate, eventPushState, eventReplaceState, eventHashchange, ]; const subscribeToLocationUpdates = (callback) => { for (const event of events) { addEventListener(event, callback); } return () => { for (const event of events) { removeEventListener(event, callback); } }; }; export const useLocationProperty = (fn, ssrFn) => useSyncExternalStore(subscribeToLocationUpdates, fn, ssrFn); const currentSearch = () => location.search; export const useSearch = ({ ssrSearch } = {}) => useLocationProperty( currentSearch, // != null checks for both null and undefined, but allows empty string "" // This allows proper hydration: server renders with ssrSearch="?foo", // client hydrates with just and reads from location.search ssrSearch != null ? () => ssrSearch : currentSearch ); const currentPathname = () => location.pathname; export const usePathname = ({ ssrPath } = {}) => useLocationProperty( currentPathname, // != null checks for both null and undefined, but allows empty string "" // This allows proper hydration: server renders with ssrPath="/foo", // client hydrates with just and reads from location.pathname ssrPath != null ? () => ssrPath : currentPathname ); const currentHistoryState = () => history.state; export const useHistoryState = () => useLocationProperty(currentHistoryState, () => null); export const navigate = (to, { replace = false, state = null } = {}) => history[replace ? eventReplaceState : eventPushState](state, "", to); // the 2nd argument of the `useBrowserLocation` return value is a function // that allows to perform a navigation. export const useBrowserLocation = (opts = {}) => [usePathname(opts), navigate]; const patchKey = Symbol.for("wouter_v3"); // While History API does have `popstate` event, the only // proper way to listen to changes via `push/replaceState` // is to monkey-patch these methods. // // See https://stackoverflow.com/a/4585031 if (typeof history !== "undefined" && typeof window[patchKey] === "undefined") { for (const type of [eventPushState, eventReplaceState]) { const original = history[type]; // TODO: we should be using unstable_batchedUpdates to avoid multiple re-renders, // however that will require an additional peer dependency on react-dom. // See: https://github.com/reactwg/react-18/discussions/86#discussioncomment-1567149 history[type] = function () { const result = original.apply(this, arguments); const event = new Event(type); event.arguments = arguments; dispatchEvent(event); return result; }; } // patch history object only once // See: https://github.com/molefrog/wouter/issues/167 Object.defineProperty(window, patchKey, { value: true }); } ================================================ FILE: packages/wouter/src/use-hash-location.d.ts ================================================ export * from "../types/use-hash-location.js"; ================================================ FILE: packages/wouter/src/use-hash-location.js ================================================ import { useSyncExternalStore } from "./react-deps.js"; // array of callback subscribed to hash updates const listeners = { v: [], }; const onHashChange = () => listeners.v.forEach((cb) => cb()); // we subscribe to `hashchange` only once when needed to guarantee that // all listeners are called synchronously const subscribeToHashUpdates = (callback) => { if (listeners.v.push(callback) === 1) addEventListener("hashchange", onHashChange); return () => { listeners.v = listeners.v.filter((i) => i !== callback); if (!listeners.v.length) removeEventListener("hashchange", onHashChange); }; }; // leading '#' is ignored, leading '/' is optional const currentHashLocation = () => "/" + location.hash.replace(/^#?\/?/, ""); export const navigate = (to, { state = null, replace = false } = {}) => { const oldURL = location.href; const [hash, search] = to.replace(/^#?\/?/, "").split("?"); // Works for ALL protocols including data: const url = new URL(location.href); url.hash = `/${hash}`; if (search) url.search = search; const newURL = url.href; if (replace) { history.replaceState(state, "", newURL); } else { history.pushState(state, "", newURL); } const event = typeof HashChangeEvent !== "undefined" ? new HashChangeEvent("hashchange", { oldURL, newURL }) : new Event("hashchange", { detail: { oldURL, newURL } }); dispatchEvent(event); }; export const useHashLocation = ({ ssrPath = "/" } = {}) => [ useSyncExternalStore( subscribeToHashUpdates, currentHashLocation, () => ssrPath ), navigate, ]; useHashLocation.hrefs = (href) => "#" + href; ================================================ FILE: packages/wouter/src/use-sync-external-store.js ================================================ export { useSyncExternalStore } from "use-sync-external-store/shim/index.js"; ================================================ FILE: packages/wouter/src/use-sync-external-store.native.js ================================================ export { useSyncExternalStore } from "use-sync-external-store/shim/index.native.js"; ================================================ FILE: packages/wouter/test/history-patch.test.ts ================================================ import { useLocation as reactHook } from "../src/index.js"; import { useLocation as preactHook } from "../src/index.js"; import { renderHook, act } from "@testing-library/react"; import { mock, test, expect, describe } from "bun:test"; describe("history patch", () => { test("exports should exists", () => { expect(reactHook).toBeDefined(); expect(preactHook).toBeDefined(); }); test("history should be patched once", () => { const fn = mock(); const { result, unmount } = renderHook(() => reactHook()); addEventListener("pushState", (e) => { fn(); }); expect(result.current[0]).toBe("/"); expect(fn).toHaveBeenCalledTimes(0); act(() => result.current[1]("/hello")); act(() => result.current[1]("/world")); expect(result.current[0]).toBe("/world"); expect(fn).toHaveBeenCalledTimes(2); unmount(); }); }); ================================================ FILE: packages/wouter/test/jest-dom.d.ts ================================================ import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers"; declare module "bun:test" { interface Matchers extends TestingLibraryMatchers {} } ================================================ FILE: packages/wouter/test/link.test-d.tsx ================================================ import { describe, expectTypeOf, test } from "bun:test"; import { Link, LinkProps, type Path } from "../src/index.js"; import * as React from "react"; type NetworkLocationHook = () => [ Path, (path: string, options: { host: string; retries?: number }) => void ]; describe(" types", () => { test("should have required prop href", () => { // @ts-expect-error test; test; }); test("does not allow `to` and `href` props to be used at the same time", () => { // @ts-expect-error Hello ; }); test("should inherit props from `HTMLAnchorElement`", () => { Hello ; Hello ; Hello ; Hello ; }); test("can accept function as `className`", () => { (isActive ? "active" : "non-active")} />; (isActive ? "active" : undefined)} />; }); test("should support other navigation params", () => { test ; test ; // @ts-expect-error Hello ; test ; }); test("should work with generic type", () => { href="/" host="wouter.com"> test ; // @ts-expect-error href="/">test; href="/" host="wouter.com" retries={4}> test ; }); }); describe(" with ref", () => { test("should work", () => { const ref = React.useRef(null); Hello ; }); test("should have error when type is miss matched", () => { const ref = React.useRef(null); // @ts-expect-error Hello ; }); }); describe(" with `asChild` prop", () => { test("should work", () => { Hello ; }); test("does not allow `to` and `href` props to be used at the same time", () => { // @ts-expect-error Hello ; }); test("can only have valid element as a child", () => { // @ts-expect-error strings are not valid children {true ? "Hello" : "World"} ; // @ts-expect-error can't use multiple nodes as children Link
    icon
    ; }); test("does not allow other props", () => { // @ts-expect-error Hello ; // @ts-expect-error Hello ; // @ts-expect-error Hello ; // @ts-expect-error Hello ; }); test("should support other navigation params", () => { Hello ; // @ts-expect-error Hello ; Hello ; }); test("should work with generic type", () => { asChild to="/" host="wouter.com">
    test
    ; // @ts-expect-error asChild to="/">
    test
    ; asChild to="/" host="wouter.com" retries={4}>
    test
    ; }); test("accepts `onClick` prop that overwrites child's handler", () => { { expectTypeOf(e).toEqualTypeOf(); }} > Hello ; }); test("should work with `ComponentProps`", () => { type LinkComponentProps = React.ComponentProps; // Because Link is a generic component, the props // cant't contain navigation options of the default generic // parameter `BrowserLocationHook`. // So the best we can get are the props such as `href` etc. expectTypeOf().toMatchTypeOf(); }); }); ================================================ FILE: packages/wouter/test/link.test.tsx ================================================ import { type MouseEventHandler } from "react"; import { test, expect, afterEach, mock, describe } from "bun:test"; import { render, cleanup, fireEvent, act } from "@testing-library/react"; import { Router, Link } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; afterEach(cleanup); describe("", () => { test("renders a link with proper attributes", () => { const { getByText } = render( Click Me ); const element = getByText("Click Me"); expect(element).toBeInTheDocument(); expect(element).toHaveAttribute("href", "/about"); expect(element).toHaveClass("link--active"); }); test("passes ref to ", () => { const refCallback = mock<(element: HTMLAnchorElement) => void>(); const { getByText } = render( Testing ); const element = getByText("Testing"); expect(element).toBeInTheDocument(); expect(element).toHaveAttribute("href", "/"); expect(refCallback).toHaveBeenCalledTimes(1); expect(refCallback).toHaveBeenCalledWith(element); }); test("still creates a plain link when nothing is passed", () => { const { getByTestId } = render(); const element = getByTestId("link"); expect(element).toBeInTheDocument(); expect(element).toHaveAttribute("href", "/about"); expect(element).toBeEmptyDOMElement(); }); test("supports `to` prop as an alias to `href`", () => { const { getByText } = render(Hello); const element = getByText("Hello"); expect(element).toBeInTheDocument(); expect(element).toHaveAttribute("href", "/about"); }); test("performs a navigation when the link is clicked", () => { const { getByTestId } = render( link ); fireEvent.click(getByTestId("link")); expect(location.pathname).toBe("/goo-baz"); }); test("supports replace navigation", () => { const { getByTestId } = render( link ); const histBefore = history.length; fireEvent.click(getByTestId("link")); expect(location.pathname).toBe("/goo-baz"); expect(history.length).toBe(histBefore); }); test("ignores the navigation when clicked with modifiers", () => { const { getByTestId } = render( click ); const clickEvt = new MouseEvent("click", { bubbles: true, cancelable: true, button: 0, ctrlKey: true, }); // js-dom doesn't implement browser navigation (e.g. changing location // when a link is clicked) so we need just ingore it to avoid warnings clickEvt.preventDefault(); fireEvent(getByTestId("link"), clickEvt); expect(location.pathname).not.toBe("/users"); }); test("ignores the navigation when event is cancelled", () => { const clickHandler: MouseEventHandler = (e) => { e.preventDefault(); }; const { getByTestId } = render( click ); fireEvent.click(getByTestId("link")); expect(location.pathname).not.toBe("/users"); }); test("accepts an `onClick` prop, fired before the navigation", () => { const clickHandler = mock(); const { getByTestId } = render( ); fireEvent.click(getByTestId("link")); expect(clickHandler).toHaveBeenCalledTimes(1); }); test("renders `href` with basepath", () => { const { getByTestId } = render( ); const link = getByTestId("link"); expect(link.getAttribute("href")).toBe("/app/dashboard"); }); test("renders `href` with absolute links", () => { const { getByTestId } = render( ); const element = getByTestId("link"); expect(element).toHaveAttribute("href", "/home"); }); test("supports history state", () => { const testState = { hello: "world" }; const { getByTestId } = render( link ); fireEvent.click(getByTestId("link")); expect(location.pathname).toBe("/goo-baz"); expect(history.state).toStrictEqual(testState); }); test("can be configured to use custom href formatting", () => { const formatter = (href: string) => `#${href}`; const { getByTestId } = render( <> ); expect(getByTestId("root")).toHaveAttribute("href", "#/"); expect(getByTestId("home")).toHaveAttribute("href", "#/home"); expect(getByTestId("absolute")).toHaveAttribute("href", "#/home"); }); }); describe("active links", () => { test("proxies `className` when it is a string", () => { const { getByText } = render( Click Me ); const element = getByText("Click Me"); expect(element).toHaveAttribute("class", "link--active warning"); }); test("calls the `className` function with active link flag", () => { const { navigate, hook } = memoryLocation({ path: "/" }); const { getByText } = render( { return [isActive ? "active" : "", "link"].join(" "); }} > Click Me ); const element = getByText("Click Me"); expect(element).toBeInTheDocument(); expect(element).toHaveClass("active"); expect(element).toHaveClass("link"); act(() => navigate("/about")); expect(element).not.toHaveClass("active"); expect(element).toHaveClass("link"); }); test("correctly highlights active links when using custom href formatting", () => { const formatter = (href: string) => `#${href}`; const { navigate, hook } = memoryLocation({ path: "/" }); const { getByText } = render( { return [isActive ? "active" : "", "link"].join(" "); }} > Click Me ); const element = getByText("Click Me"); expect(element).toBeInTheDocument(); expect(element).toHaveClass("active"); expect(element).toHaveClass("link"); act(() => navigate("/about")); expect(element).not.toHaveClass("active"); expect(element).toHaveClass("link"); }); }); describe(" with `asChild` prop", () => { test("when `asChild` is not specified, wraps the children in an ", () => { const { getByText } = render(
    Click Me
    ); const link = getByText("Click Me"); expect(link.tagName).toBe("DIV"); expect(link).not.toHaveAttribute("href"); expect(link).toHaveClass("link--wannabe"); expect(link).toHaveTextContent("Click Me"); expect(link.parentElement?.tagName).toBe("A"); expect(link.parentElement).toHaveAttribute("href", "/about"); }); test("when invalid element is provided, wraps the children in an
    ", () => { const { getByText } = render( /* @ts-expect-error */ Click Me ); const link = getByText("Click Me"); expect(link.tagName).toBe("A"); expect(link).toHaveAttribute("href", "/about"); expect(link).toHaveTextContent("Click Me"); }); test("when more than one element is provided, wraps the children in an ", async () => { const { getByText } = render( /* @ts-expect-error */ 1 2 3 ); const span = getByText("1"); expect(span.parentElement?.tagName).toBe("A"); expect(span.parentElement).toHaveAttribute("href", "/about"); expect(span.parentElement).toHaveTextContent("123"); }); test("injects href prop when rendered with `asChild`", () => { const { getByText } = render(
    Click Me
    ); const link = getByText("Click Me"); expect(link.tagName).toBe("DIV"); expect(link).toHaveClass("link--wannabe"); expect(link).toHaveAttribute("href", "/about"); expect(link).toHaveTextContent("Click Me"); }); test("missing href or to won't crash", () => { const { getByText } = render( /* @ts-expect-error */ Click Me ); const link = getByText("Click Me"); expect(link.tagName).toBe("A"); expect(link).toHaveAttribute("href", undefined); expect(link).toHaveTextContent("Click Me"); }); }); ================================================ FILE: packages/wouter/test/location-hook.test-d.ts ================================================ import { test, expectTypeOf, describe } from "bun:test"; import { BaseLocationHook, HookNavigationOptions } from "../src/index.js"; describe("`HookNavigationOptions` utility type", () => { test("should return empty interface for hooks with no nav options", () => { const hook = (): [string, (path: string) => void] => { return ["stub", (path: string) => {}]; }; type Options = HookNavigationOptions; expectTypeOf().toEqualTypeOf<{}>(); const optionsExt: Options | { a: 1 } = { a: 1, b: 2 }; }); test("should return object with required navigation params", () => { const hook = (): [ string, (path: string, options: { replace: boolean; optional?: number }) => void ] => { return ["stub", () => {}]; }; type Options = HookNavigationOptions; // @ts-expect-error expectTypeOf().toEqualTypeOf<{ replace: boolean; foo: string; }>(); expectTypeOf().toEqualTypeOf<{ replace: boolean; optional?: number; }>(); }); test("should not contain never when options are optional", () => { const hook = ( param: string ): [string, (path: string, options?: { replace: boolean }) => void] => { return ["stub", () => {}]; }; type Options = HookNavigationOptions; expectTypeOf().toEqualTypeOf<{ replace: boolean; }>(); }); test("should only support valid hooks", () => { // @ts-expect-error type A = HookNavigationOptions; // @ts-expect-error type B = HookNavigationOptions<{}>; // @ts-expect-error type C = HookNavigationOptions<() => []>; }); test("should return empty object when `BaseLocationHook` is given", () => { type Options = HookNavigationOptions; expectTypeOf().toEqualTypeOf<{}>(); }); }); ================================================ FILE: packages/wouter/test/match-route.test-d.ts ================================================ import { test, expectTypeOf } from "bun:test"; import { matchRoute, useRouter } from "../src/index.js"; const assertType = (_value: T): void => {}; const { parser } = useRouter(); test("should only accept strings", () => { // @ts-expect-error assertType(matchRoute(parser, Symbol(), "")); // @ts-expect-error assertType(matchRoute(parser, undefined, "")); assertType(matchRoute(parser, "/", "")); }); test('has a boolean "match" result as a first returned value', () => { const [match] = matchRoute(parser, "/", ""); expectTypeOf(match).toEqualTypeOf(); }); test("returns null as parameters when there was no match", () => { const [match, params] = matchRoute(parser, "/foo", ""); if (!match) { expectTypeOf(params).toEqualTypeOf(); } }); test("accepts the type of parameters as a generic argument", () => { const [match, params] = matchRoute<{ id: string; name: string | undefined }>( parser, "/app/users/:name?/:id", "" ); if (match) { expectTypeOf(params).toEqualTypeOf<{ id: string; name: string | undefined; }>(); } }); test("infers parameters from the route path", () => { const [, inferedParams] = matchRoute(parser, "/app/users/:name?/:id/*?", ""); if (inferedParams) { expectTypeOf(inferedParams).toMatchTypeOf<{ 0?: string; 1?: string; 2?: string; name?: string; id: string; wildcard?: string; }>(); } }); ================================================ FILE: packages/wouter/test/memory-location.test-d.ts ================================================ import { test, expectTypeOf } from "bun:test"; import { memoryLocation } from "../src/memory-location.js"; import { BaseLocationHook } from "../src/index.js"; const assertType = (_value: T): void => {}; test("should return hook that supports location spec", () => { const { hook } = memoryLocation(); expectTypeOf(hook).toMatchTypeOf(); const [location, navigate] = hook(); assertType(location); assertType(navigate); }); test("should return `navigate` method for navigating outside of components", () => { const { navigate } = memoryLocation(); assertType(navigate); }); test("should support `record` option for saving the navigation history", () => { const { history, reset } = memoryLocation({ record: true }); assertType(history); assertType(reset); }); test("should have history only wheen record is true", () => { // @ts-expect-error const { history, reset } = memoryLocation({ record: false }); assertType(history); assertType(reset); }); test("should support initial path", () => { const { hook } = memoryLocation({ path: "/initial-path" }); expectTypeOf(hook).toMatchTypeOf(); }); test("should support `static` option", () => { const { hook } = memoryLocation({ static: true }); expectTypeOf(hook).toMatchTypeOf(); }); ================================================ FILE: packages/wouter/test/memory-location.test.ts ================================================ import { test, expect } from "bun:test"; import { renderHook, act } from "@testing-library/react"; import { memoryLocation } from "../src/memory-location.js"; test("returns a hook that is compatible with location spec", () => { const { hook } = memoryLocation(); const { result, unmount } = renderHook(() => hook()); const [value, update] = result.current; expect(typeof value).toBe("string"); expect(typeof update).toBe("function"); unmount(); }); test("should support initial path", () => { const { hook } = memoryLocation({ path: "/test-case" }); const { result, unmount } = renderHook(() => hook()); const [value] = result.current; expect(value).toBe("/test-case"); unmount(); }); test("should support initial path with query", () => { const { searchHook } = memoryLocation({ path: "/test-case?foo=bar" }); const { result, unmount } = renderHook(() => searchHook()); const value = result.current; expect(value).toBe("foo=bar"); unmount(); }); test("should support search path as parameter", () => { const { searchHook } = memoryLocation({ path: "/test-case?foo=bar", searchPath: "key=value", }); const { result, unmount } = renderHook(() => searchHook()); const value = result.current; expect(value).toBe("foo=bar&key=value"); unmount(); }); test('should return location hook that has initial path "/" by default', () => { const { hook } = memoryLocation(); const { result, unmount } = renderHook(() => hook()); const [value] = result.current; expect(value).toBe("/"); unmount(); }); test('should return search hook that has initial query "" by default', () => { const { searchHook } = memoryLocation(); const { result, unmount } = renderHook(() => searchHook()); const value = result.current; expect(value).toBe(""); unmount(); }); test("should return standalone `navigate` method", () => { const { hook, navigate } = memoryLocation(); const { result, unmount } = renderHook(() => hook()); act(() => navigate("/standalone")); const [value] = result.current; expect(value).toBe("/standalone"); unmount(); }); test("should return location hook that supports navigation", () => { const { hook } = memoryLocation(); const { result, unmount } = renderHook(() => hook()); act(() => result.current[1]("/location")); const [value] = result.current; expect(value).toBe("/location"); unmount(); }); test("should record all history when `record` option is provided", () => { const { hook, history, navigate: standalone, } = memoryLocation({ record: true, path: "/test" }); const { result, unmount } = renderHook(() => hook()); act(() => standalone("/standalone")); act(() => result.current[1]("/location")); expect(result.current[0]).toBe("/location"); expect(history).toStrictEqual(["/test", "/standalone", "/location"]); act(() => standalone("/standalone", { replace: true })); expect(history).toStrictEqual(["/test", "/standalone", "/standalone"]); act(() => result.current[1]("/location", { replace: true })); expect(history).toStrictEqual(["/test", "/standalone", "/location"]); unmount(); }); test("should not have history when `record` option is falsy", () => { // @ts-expect-error const { history, reset } = memoryLocation(); expect(history).not.toBeDefined(); expect(reset).not.toBeDefined(); }); test("should have reset method when `record` option is provided", () => { const { history, reset, navigate } = memoryLocation({ path: "/initial", record: true, }); expect(history).toBeDefined(); expect(reset).toBeDefined(); navigate("test-1"); navigate("test-2"); reset(); expect(history).toStrictEqual(["/initial"]); }); test("should have reset method that reset hook location", () => { const { hook, history, navigate, reset } = memoryLocation({ record: true, path: "/test", }); const { result, unmount } = renderHook(() => hook()); act(() => navigate("/location")); expect(result.current[0]).toBe("/location"); expect(history).toStrictEqual(["/test", "/location"]); act(() => reset()); expect(history).toStrictEqual(["/test"]); expect(result.current[0]).toBe("/test"); unmount(); }); ================================================ FILE: packages/wouter/test/nested-route.test.tsx ================================================ import { test, expect, describe } from "bun:test"; import { act, render, renderHook } from "@testing-library/react"; import { Route, Router, Switch, useRouter } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; describe("when `nest` prop is given", () => { test("renders by default", () => { const { container } = render(matched!); expect(container.innerHTML).toBe("matched!"); }); test("matches the pattern loosely", () => { const { hook, navigate } = memoryLocation(); const { container } = render( matched! ); expect(container.innerHTML).toBe(""); act(() => navigate("/posts/all")); // full match expect(container.innerHTML).toBe("matched!"); act(() => navigate("/users")); expect(container.innerHTML).toBe(""); act(() => navigate("/posts/10-react-tricks/table-of-contents")); expect(container.innerHTML).toBe("matched!"); }); test("can be used inside a Switch", () => { const { container } = render( about nested default ); expect(container.innerHTML).toBe("nested"); }); test("sets the base to the matched segment", () => { const { result } = renderHook(() => useRouter().base, { wrapper: (props) => ( {props.children} ), }); expect(result.current).toBe("/2012/04"); }); test("can be nested in another nested `Route` or `Router`", () => { const { container } = render( should not be rendered All settings ); expect(container.innerHTML).toBe("All settings"); }); test("reacts to `nest` updates", () => { const { hook } = memoryLocation({ path: "/app/apple/products", static: true, }); const App = ({ nested }: { nested: boolean }) => { return ( matched! ); }; const { container, rerender } = render(); expect(container.innerHTML).toBe("matched!"); rerender(); expect(container.innerHTML).toBe(""); }); test("works with one optional segment", () => { const { hook, navigate } = memoryLocation({ path: "/", }); const App = () => { return ( {({ version }) => version ?? "default"} ); }; const { container } = render(); expect(container.innerHTML).toBe("default"); act(() => navigate("/v1")); expect(container.innerHTML).toBe("v1"); act(() => navigate("/v2/dashboard")); expect(container.innerHTML).toBe("v2"); }); }); ================================================ FILE: packages/wouter/test/parser.test.tsx ================================================ import { test, expect } from "bun:test"; import { pathToRegexp, Key } from "path-to-regexp"; import { renderHook } from "@testing-library/react"; import { Router, useRouter, useRoute, Parser } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; // Custom parser that uses `path-to-regexp` instead of `regexparam` const pathToRegexpParser: Parser = (route: string) => { const keys: Key[] = []; const pattern = pathToRegexp(route, keys); return { pattern, keys: keys.map((k) => String(k.name)) }; }; test("overrides the `parser` prop on the current router", () => { const { result } = renderHook(() => useRouter(), { wrapper: ({ children }) => ( {children} ), }); const router = result.current; expect(router.parser).toBe(pathToRegexpParser); }); test("allows to change the behaviour of route matching", () => { const { result } = renderHook( () => useRoute("/(home|dashboard)/:pages?/users/:rest*"), { wrapper: ({ children }) => ( {children} ), } ); expect(result.current).toStrictEqual([ true, { 0: "home", 1: undefined, 2: "10/bio", pages: undefined, rest: "10/bio", } as any, ]); }); ================================================ FILE: packages/wouter/test/redirect.test-d.tsx ================================================ import { describe, test } from "bun:test"; import { Redirect } from "../src/index.js"; const assertType = (_value: T): void => {}; describe("Redirect types", () => { test("should have required prop href", () => { // @ts-expect-error assertType(); assertType(); }); test("should support state prop", () => { assertType(); assertType(); assertType(); assertType(); }); test("always renders nothing", () => { // can be used in JSX
    ; assertType(Redirect({ href: "/" })); }); test("can not accept children", () => { // @ts-expect-error hi!; // prettier-ignore // @ts-expect-error <>
    Fragment
    ; }); }); ================================================ FILE: packages/wouter/test/redirect.test.tsx ================================================ import { test, expect } from "bun:test"; import { render } from "@testing-library/react"; import { useState } from "react"; import { Redirect, Router } from "../src/index.js"; export const customHookWithReturn = (initialPath = "/") => () => { const [path, updatePath] = useState(initialPath); const navigate = (path: string) => { updatePath(path); return "foo"; }; return [path, navigate]; }; test("renders nothing", () => { const { container, unmount } = render(); expect(container.childNodes.length).toBe(0); unmount(); }); test("results in change of current location", () => { const { unmount } = render(); expect(location.pathname).toBe("/users"); unmount(); }); test("supports `base` routers with relative path", () => { const { unmount } = render( ); expect(location.pathname).toBe("/app/nested"); unmount(); }); test("supports `base` routers with absolute path", () => { const { unmount } = render( ); expect(location.pathname).toBe("/absolute"); unmount(); }); test("supports replace navigation", () => { const histBefore = history.length; const { unmount } = render(); expect(location.pathname).toBe("/users"); expect(history.length).toBe(histBefore); unmount(); }); test("supports history state", () => { const testState = { hello: "world" }; const { unmount } = render(); expect(location.pathname).toBe("/users"); expect(history.state).toStrictEqual(testState); unmount(); }); test("useLayoutEffect should return nothing", () => { const { unmount } = render( // @ts-expect-error ); expect(location.pathname).toBe("/users"); unmount(); }); ================================================ FILE: packages/wouter/test/route.test-d.tsx ================================================ import { test, describe, expectTypeOf } from "bun:test"; import { Route } from "../src/index.js"; import { ComponentProps } from "react"; import * as React from "react"; const assertType = (_value: T): void => {}; describe("`path` prop", () => { test("is optional", () => { assertType(); }); test("should be a string or RegExp", () => { let a: ComponentProps["path"]; expectTypeOf(a).toMatchTypeOf(); }); }); test("accepts the optional boolean `nest` prop", () => { assertType(); assertType(); // @ts-expect-error - should be boolean assertType(); }); test("renders a component provided in the `component` prop", () => { const Header = () =>
    ; const Profile = () => null; ; ; // @ts-expect-error must be a component, not JSX } />; }); test("accepts class components in the `component` prop", () => { class A extends React.Component<{ params: {} }> { render() { return
    ; } } ; }); test("accepts ForwardRefExoticComponent in the `component` prop", () => { // Simulates components wrapped with HOCs like withErrorBoundary const MyComponent = React.forwardRef( ({ params }) =>
    ); ; }); test("accepts children", () => {
    ; This is a mixed content ; <>
    ; }); test("supports functions as children", () => { {(params) => { expectTypeOf(params).toMatchTypeOf<{}>(); return
    ; }} ; {({ id }) => `User id: ${id}`}; {({ age }: { age: string }) => `User age: ${age}`} ; // @ts-expect-error function should return valid JSX {() => {}}; // prettier-ignore // @ts-expect-error you can't use JSX together with render function {() =>
    }Link; }); describe("parameter inference", () => { test("can infer type of params from the path given", () => { {({ first, second, third }) => { expectTypeOf(first).toEqualTypeOf(); return
    {`${first}, ${second}, ${third}`}
    ; }}
    ; {/* @ts-expect-error - `age` param is not present in the pattern */} {({ name, age }) => { return
    {`Hello, ${name}`}
    ; }}
    ; }); test("extract wildcard params into `wild` property", () => { {({ wild }) => { expectTypeOf(wild).toEqualTypeOf(); return
    The path is {wild}
    ; }}
    ; }); test("allows to customize type of params via generic parameter", () => { path="/users/:name/:age"> {(params) => { expectTypeOf(params.lastName).toEqualTypeOf(); return
    This really is undefined {params.lastName}
    ; }} ; }); test("can't infer the type when the path isn't known at compile time", () => { {(params) => { // @ts-expect-error params.section; return
    ; }} ; }); }); ================================================ FILE: packages/wouter/test/route.test.tsx ================================================ import { it, expect, afterEach } from "bun:test"; import { render, act, cleanup } from "@testing-library/react"; import { Router, Route } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; import { ReactElement } from "react"; // Clean up after each test to avoid DOM pollution afterEach(cleanup); const testRouteRender = (initialPath: string, jsx: ReactElement) => { return render( {jsx} ); }; it("always renders its content when `path` is empty", () => { const { container } = testRouteRender( "/nothing",

    Hello!

    ); const heading = container.querySelector("h1"); expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent("Hello!"); }); it("accepts plain children", () => { const { container } = testRouteRender( "/foo",

    Hello!

    ); const heading = container.querySelector("h1"); expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent("Hello!"); }); it("works with render props", () => { const { container } = testRouteRender( "/foo", {() =>

    Hello!

    }
    ); const heading = container.querySelector("h1"); expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent("Hello!"); }); it("passes a match param object to the render function", () => { const { container } = testRouteRender( "/users/alex", {(params) =>

    {params.name}

    }
    ); const heading = container.querySelector("h1"); expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent("alex"); }); it("renders nothing when there is not match", () => { const { container } = testRouteRender( "/bar",
    Hi!
    ); expect(container.querySelector("div")).not.toBeInTheDocument(); }); it("supports `component` prop similar to React-Router", () => { const Users = () =>

    All users

    ; const { container } = testRouteRender( "/foo", ); const heading = container.querySelector("h2"); expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent("All users"); }); it("supports `base` routers with relative path", () => { const { container, unmount } = render(

    Nested

    Absolute

    ); act(() => history.replaceState(null, "", "/app/nested")); expect(container.children).toHaveLength(1); expect(container.firstChild).toHaveProperty("tagName", "H1"); unmount(); }); it("supports `path` prop with regex", () => { const { container } = testRouteRender( "/foo",

    Hello!

    ); const heading = container.querySelector("h1"); expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent("Hello!"); }); it("supports regex path named params", () => { const { container } = testRouteRender( "/users/alex", [a-z]+)/}> {(params) =>

    {params.name}

    }
    ); const heading = container.querySelector("h1"); expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent("alex"); }); it("supports regex path anonymous params", () => { const { container } = testRouteRender( "/users/alex", {(params) =>

    {params[0]}

    }
    ); const heading = container.querySelector("h1"); expect(heading).toBeInTheDocument(); expect(heading).toHaveTextContent("alex"); }); it("rejects when a path does not match the regex", () => { const { container } = testRouteRender( "/users/1234", [a-z]+)/}> {(params) =>

    {params.name}

    }
    ); expect(container.querySelector("h1")).not.toBeInTheDocument(); }); ================================================ FILE: packages/wouter/test/router.test-d.tsx ================================================ import { ComponentProps } from "react"; import { test, expectTypeOf } from "bun:test"; import { Router, Route, BaseLocationHook, useRouter, Parser, Path, } from "../src/index.js"; test("should have at least one child", () => { // @ts-expect-error ; }); test("accepts valid elements as children", () => { const Header = ({ title }: { title: string }) =>

    {title}

    ; Hello! ; Hello, we have
    and some {1337} numbers here. ; <>Fragments! ; {/* @ts-expect-error should be a valid element */} {() =>
    } ; }); test("can be customized with router properties passed as props", () => { // @ts-expect-error ; const useFakeLocation: BaseLocationHook = () => ["/foo", () => {}]; this is a valid router; let fn: ComponentProps["hook"]; expectTypeOf(fn).exclude().toBeFunction(); Hello World!; SSR; Custom ; }); test("accepts `hrefs` function for transforming href strings", () => { const router = useRouter(); expectTypeOf(router.hrefs).toBeFunction(); href + "1"}>0; { expectTypeOf(router).toEqualTypeOf(); return href + router.base; }} > routers as a second argument ; }); test("accepts `parser` function for generating regular expressions", () => { const parser: Parser = (path: Path, loose?: boolean) => { return { pattern: new RegExp(`^${path}${loose === true ? "(?=$|[/])" : "[/]$"}`), keys: [], }; }; this is a valid router; }); test("does not accept other props", () => { const router = useRouter(); // @ts-expect-error `parent` prop isn't defined Parent router; }); ================================================ FILE: packages/wouter/test/router.test.tsx ================================================ import { memo, ReactElement, cloneElement, ComponentProps } from "react"; import { renderHook, render } from "@testing-library/react"; import { it, expect, describe } from "bun:test"; import { Router, DefaultParams, useRouter, Parser, BaseLocationHook, } from "../src/index.js"; it("creates a router object on demand", () => { const { result } = renderHook(() => useRouter()); expect(result.current).toBeInstanceOf(Object); }); it("creates a router object only once", () => { const { result, rerender } = renderHook(() => useRouter()); const router = result.current; rerender(); expect(result.current).toBe(router); }); it("does not create new router when rerenders", () => { const { result, rerender } = renderHook(() => useRouter(), { wrapper: (props) => {props.children}, }); const router = result.current; rerender(); expect(result.current).toBe(router); }); it("alters the current router with `parser` and `hook` options", () => { const newParser: Parser = () => ({ pattern: /(.*)/, keys: [] }); const hook: BaseLocationHook = () => ["/foo", () => {}]; const { result } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); const router = result.current; expect(router).toBeInstanceOf(Object); expect(router.parser).toBe(newParser); expect(router.hook).toBe(hook); }); it("accepts `ssrPath` and `ssrSearch` params", () => { const { result } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); expect(result.current.ssrPath).toBe("/users"); expect(result.current.ssrSearch).toBe("a=b&c=d"); }); it("can extract `ssrSearch` from `ssrPath` after the '?' symbol", () => { let ssrPath: string | undefined = "/no-search"; let ssrSearch: string | undefined = undefined; const { result, rerender } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); expect(result.current.ssrPath).toBe("/no-search"); expect(result.current.ssrSearch).toBe(""); ssrPath = "/with-search?a=b&c=d"; rerender(); expect(result.current.ssrPath).toBe("/with-search"); expect(result.current.ssrSearch).toBe("a=b&c=d"); ssrSearch = "x=y&z=w"; rerender(); expect(result.current.ssrSearch).toBe("a=b&c=d"); }); it("keeps the ssrSearch undefined if not in SSR mode", () => { const { result } = renderHook(() => useRouter(), { wrapper: (props) => {props.children}, }); expect(result.current.ssrPath).toBe(undefined); expect(result.current.ssrSearch).toBe(undefined); }); it("shares one router instance between components", () => { const routers: any[] = []; const RouterGetter = ({ index }: { index: number }) => { const router = useRouter(); routers[index] = router; return
    ; }; render( <> ); const uniqRouters = [...new Set(routers)]; expect(uniqRouters.length).toBe(1); }); describe("`base` prop", () => { it("is an empty string by default", () => { const { result } = renderHook(() => useRouter()); expect(result.current.base).toBe(""); }); it("can be customized via the `base` prop", () => { const { result } = renderHook(() => useRouter(), { wrapper: (props) => {props.children}, }); expect(result.current.base).toBe("/foo"); }); it("appends provided path to the parent router's base", () => { const { result } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); expect(result.current.base).toBe("/baz/foo/bar"); }); }); describe("`hook` prop", () => { it("when provided, the router isn't inherited from the parent", () => { const customHook: BaseLocationHook = () => ["/foo", () => {}]; const newParser: Parser = () => ({ pattern: /(.*)/, keys: [] }); const { result: { current: router }, } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); expect(router.hook).toBe(customHook); expect(router.parser).not.toBe(newParser); expect(router.base).toBe("/bar"); }); }); describe("`hrefs` prop", () => { it("sets the router's `hrefs` property", () => { const formatter = () => "noop"; const { result: { current: router }, } = renderHook(() => useRouter(), { wrapper: (props) => {props.children}, }); expect(router.hrefs).toBe(formatter); }); it("can infer `hrefs` from the `hook`", () => { const hookHrefs = () => "noop"; const hook = (): [string, (v: string) => void] => { return ["/foo", () => {}]; }; hook.hrefs = hookHrefs; let hrefsRouterOption: ((href: string) => string) | undefined; const { rerender, result } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); expect(result.current.hrefs).toBe(hookHrefs); // `hrefs` passed directly to the router should take precedence hrefsRouterOption = (href) => "custom formatter"; rerender(); expect(result.current.hrefs).toBe(hrefsRouterOption); }); }); describe("`aroundNav` prop", () => { it("sets the router's `aroundNav` property", () => { const aroundNav = () => {}; const { result } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); expect(result.current.aroundNav).toBe(aroundNav); }); it("is inherited from parent router", () => { const aroundNav = () => {}; const { result } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); expect(result.current.aroundNav).toBe(aroundNav); }); it("can be overridden in nested router", () => { const parentAroundNav = () => {}; const childAroundNav = () => {}; const { result } = renderHook(() => useRouter(), { wrapper: (props) => ( {props.children} ), }); expect(result.current.aroundNav).toBe(childAroundNav); }); }); it("updates the context when settings are changed", () => { const state: { renders: number } & Partial> = { renders: 0, }; const Memoized = memo((props) => { const router = useRouter(); state.renders++; state.hook = router.hook; state.base = router.base; return <>; }); const { rerender } = render( ); expect(state.renders).toEqual(1); expect(state.base).toBe("/app"); rerender( ); expect(state.renders).toEqual(1); // nothing changed // should re-render the hook const newHook: BaseLocationHook = () => ["/location", () => {}]; rerender( ); expect(state.renders).toEqual(2); expect(state.base).toEqual("/app"); expect(state.hook).toEqual(newHook); // should update the context when the base changes as well rerender( ); expect(state.renders).toEqual(3); expect(state.base).toEqual(""); expect(state.hook).toEqual(newHook); // the last check that the router context is stable during re-renders rerender( ); expect(state.renders).toEqual(3); // nothing changed }); ================================================ FILE: packages/wouter/test/setup.ts ================================================ import { GlobalRegistrator } from "@happy-dom/global-registrator"; import { expect } from "bun:test"; import * as matchers from "@testing-library/jest-dom/matchers"; // Register happy-dom globals (document, window, etc.) GlobalRegistrator.register({ url: "https://wouter.dev", width: 1024, height: 768, }); // Extend Bun's expect with jest-dom matchers (expect as any).extend(matchers); /** * Runs a function with `location` temporarily removed from globalThis. * Simulates pure Node.js SSR environment for testing. */ export const withoutLocation = (fn: () => T): T => { const original = globalThis.location; // @ts-expect-error - intentionally removing location delete globalThis.location; try { return fn(); } finally { globalThis.location = original; } }; ================================================ FILE: packages/wouter/test/ssr.test.tsx ================================================ import { test, expect, describe } from "bun:test"; import { renderToStaticMarkup } from "react-dom/server"; import { Route, Router, useRoute, Link, Redirect, useSearch, useLocation, SsrContext, } from "../src/index.js"; import { withoutLocation } from "./setup.js"; describe("server-side rendering", () => { test("works via `ssrPath` prop", () => { const App = () => ( foo bar {(params) => params.id} should not be rendered ); const rendered = renderToStaticMarkup(); expect(rendered).toBe("foobarbaz"); }); test("supports hook-based routes", () => { const HookRoute = () => { const [match, params] = useRoute("/pages/:name"); return <>{match ? `Welcome to ${params.name}!` : "Not Found!"}; }; const App = () => ( ); const rendered = renderToStaticMarkup(); expect(rendered).toBe("Welcome to intro!"); }); test("renders valid and accessible link elements", () => { const App = () => ( Mark ); const rendered = renderToStaticMarkup(); expect(rendered).toBe(`Mark`); }); test("renders redirects however they have effect only on a client-side", () => { const App = () => ( You won't see that in SSR page ); const rendered = renderToStaticMarkup(); expect(rendered).toBe(""); }); test("update ssr context", () => { const context: SsrContext = {}; const App = () => ( ); renderToStaticMarkup(); expect(context.redirectTo).toBe("/foo"); // Clean up - reset context to prevent state leakage delete context.redirectTo; }); describe("rendering with given search string", () => { test("allows to override search string", () => { const App = () => { const search = useSearch(); const [location] = useLocation(); return ( <> {location} filter by {search} ); }; const rendered = renderToStaticMarkup( ); expect(rendered).toBe("/catalog filter by sort=created_at"); }); test("doesn't break useSearch hook if not specified", () => { const PrintSearch = () => <>{useSearch()}; const rendered = withoutLocation(() => renderToStaticMarkup( ) ); expect(rendered).toBe(""); }); test("works with empty ssrSearch", () => { const PrintSearch = () => <>{useSearch()}; const rendered = withoutLocation(() => renderToStaticMarkup( ) ); expect(rendered).toBe(""); }); }); }); ================================================ FILE: packages/wouter/test/switch.test.tsx ================================================ import { it, expect, afterEach } from "bun:test"; import { Router, Route, Switch } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; import { render, act, cleanup } from "@testing-library/react"; import { PropsWithChildren, ReactElement, JSX } from "react"; // Clean up after each test to avoid DOM pollution afterEach(cleanup); const raf = () => new Promise((resolve) => requestAnimationFrame(resolve)); const testRouteRender = (initialPath: string, jsx: ReactElement) => { return render( {jsx} ); }; it("works well when nothing is provided", () => { const { container } = testRouteRender("/users/12", {null}); // When Switch has no matching children, it renders null, so container should be empty expect(container).toBeEmptyDOMElement(); }); it("always renders no more than 1 matched children", () => { const { container } = testRouteRender( "/users/12",

    ); // Should only render the h2 that matches /users/:id expect(container.querySelectorAll("h1, h2, h3")).toHaveLength(1); expect(container.querySelector("h2")).toBeInTheDocument(); expect(container.querySelector("h1")).not.toBeInTheDocument(); expect(container.querySelector("h3")).not.toBeInTheDocument(); }); it("ignores mixed children", () => { const { container } = testRouteRender( "/users", Here is aroute route ); // Should only render the route content, ignoring text nodes expect(container).toHaveTextContent("route"); // The text "Here is a" and "route" outside the Route should be ignored expect(container.textContent).toBe("route"); }); it("ignores falsy children", () => { const { container } = testRouteRender( "/users", {""} {false} {null} {undefined} route ); // Should only render the route content expect(container).toHaveTextContent("route"); expect(container.textContent).toBe("route"); }); it("matches regular components as well", () => { const Dummy = (props: PropsWithChildren<{ path: string }>) => ( <>{props.children} ); const { container } = testRouteRender( "/", Component Bold ); // Should render the Dummy component content expect(container).toHaveTextContent("Component"); expect(container.querySelector("b")).not.toBeInTheDocument(); }); it("allows to specify which routes to render via `location` prop", () => { const { container } = testRouteRender( "/something-different", route ); // Should render based on the location prop, not the actual path expect(container).toHaveTextContent("route"); }); it("always ensures the consistency of inner routes rendering", async () => { history.replaceState(null, "", "/foo/bar"); const { unmount } = render( {(params) => { if (!params) throw new Error("Render prop is called with falsy params!"); return null; }} ); await act(async () => { await raf(); history.pushState(null, "", "/"); }); unmount(); }); it("supports catch-all routes with wildcard segments", async () => { const { container } = testRouteRender( "/something-different",

    ); // Should match the catch-all route expect(container.querySelectorAll("h1, h2")).toHaveLength(1); expect(container.querySelector("h2")).toBeInTheDocument(); expect(container.querySelector("h1")).not.toBeInTheDocument(); }); it("uses a route without a path prop as a fallback", async () => { const { container } = testRouteRender( "/something-different",

    ); // Should match the fallback route (no path) expect(container.querySelectorAll("h1, h2")).toHaveLength(1); expect(container.querySelector("h2")).toBeInTheDocument(); expect(container.querySelector("h1")).not.toBeInTheDocument(); }); it("correctly handles arrays as children", async () => { const { container } = testRouteRender( "/in-array-3", {[1, 2, 3].map((i) => { const H = `h${i}` as keyof JSX.IntrinsicElements; return ( ); })}

    ); // Should match the third route (/in-array-3) expect(container.querySelectorAll("h1, h2, h3, h4")).toHaveLength(1); expect(container.querySelector("h3")).toBeInTheDocument(); expect(container.querySelector("h1")).not.toBeInTheDocument(); expect(container.querySelector("h2")).not.toBeInTheDocument(); expect(container.querySelector("h4")).not.toBeInTheDocument(); }); it("correctly handles fragments as children", async () => { const { container } = testRouteRender( "/in-fragment-2", <> {[1, 2, 3].map((i) => { const H = `h${i}` as keyof JSX.IntrinsicElements; return ( ); })}

    ); // Should match the second route (/in-fragment-2) expect(container.querySelectorAll("h1, h2, h3, h4")).toHaveLength(1); expect(container.querySelector("h2")).toBeInTheDocument(); expect(container.querySelector("h1")).not.toBeInTheDocument(); expect(container.querySelector("h3")).not.toBeInTheDocument(); expect(container.querySelector("h4")).not.toBeInTheDocument(); }); ================================================ FILE: packages/wouter/test/test-utils.ts ================================================ /** * Executes a callback and returns a promise that resolve when `hashchange` event is fired. * Rejects after `throwAfter` milliseconds. */ export const waitForHashChangeEvent = async ( cb: () => void, throwAfter = 1000 ) => new Promise((resolve, reject) => { let timeout: ReturnType; const onChange = () => { resolve(); clearTimeout(timeout); window.removeEventListener("hashchange", onChange); }; window.addEventListener("hashchange", onChange); cb(); timeout = setTimeout(() => { reject(new Error("Timed out: `hashchange` event did not fire!")); window.removeEventListener("hashchange", onChange); }, throwAfter); }); ================================================ FILE: packages/wouter/test/use-browser-location.test-d.ts ================================================ import { test, describe, expectTypeOf } from "bun:test"; import { useBrowserLocation, useSearch, useHistoryState, } from "../src/use-browser-location.js"; const assertType = (_value: T): void => {}; describe("useBrowserLocation", () => { test("should return string, function tuple", () => { const [loc, navigate] = useBrowserLocation(); assertType(loc); assertType(navigate); }); test("should return `navigate` function with `path` and `options` parameters", () => { const [, navigate] = useBrowserLocation(); assertType(navigate("/path")); assertType(navigate("")); // @ts-expect-error assertType(navigate()); // @ts-expect-error assertType(navigate(null)); assertType(navigate("/path", { replace: true })); // @ts-expect-error assertType(navigate("/path", { unknownOption: true })); }); test("should support `ssrPath` option", () => { assertType(useBrowserLocation({ ssrPath: "/something" })); // @ts-expect-error assertType(useBrowserLocation({ foo: "bar" })); }); }); describe("useSearch", () => { test("should return string", () => { type Search = ReturnType; const search = useSearch(); assertType(search); const allowedSearchValues: Search[] = ["", "?leading", "no-?-sign"]; }); }); describe("useHistoryState", () => { test("should support generics", () => { type TestCase = { hello: string }; const state = useHistoryState(); expectTypeOf(state).toEqualTypeOf(); }); test("should fallback to any when type doesn't provided", () => { const state = useHistoryState(); expectTypeOf(state).toEqualTypeOf(); }); }); ================================================ FILE: packages/wouter/test/use-browser-location.test.tsx ================================================ import { useEffect } from "react"; import { test, expect, describe, beforeEach, afterEach } from "bun:test"; import { renderHook, act, waitFor, cleanup } from "@testing-library/react"; import { useBrowserLocation, navigate, useSearch, useHistoryState, } from "../src/use-browser-location.js"; afterEach(cleanup); test("returns a pair [value, update]", () => { const { result, unmount } = renderHook(() => useBrowserLocation()); const [value, update] = result.current; expect(typeof value).toBe("string"); expect(typeof update).toBe("function"); unmount(); }); describe("`value` first argument", () => { beforeEach(() => history.replaceState(null, "", "/")); test("reflects the current pathname", () => { const { result, unmount } = renderHook(() => useBrowserLocation()); expect(result.current[0]).toBe("/"); unmount(); }); test("reacts to `pushState` / `replaceState`", () => { const { result, unmount } = renderHook(() => useBrowserLocation()); act(() => history.pushState(null, "", "/foo")); expect(result.current[0]).toBe("/foo"); act(() => history.replaceState(null, "", "/bar")); expect(result.current[0]).toBe("/bar"); unmount(); }); test("supports history.back() navigation", async () => { const { result, unmount } = renderHook(() => useBrowserLocation()); act(() => history.pushState(null, "", "/foo")); await waitFor(() => expect(result.current[0]).toBe("/foo")); act(() => { history.back(); }); // Workaround for happy-dom: manually dispatch popstate event // happy-dom doesn't fully implement history.back() popstate events act(() => { const popstateEvent = new PopStateEvent("popstate", { state: history.state, }); window.dispatchEvent(popstateEvent); }); await waitFor(() => expect(result.current[0]).toBe("/"), { timeout: 1000 }); unmount(); }); test("supports history state", () => { const { result, unmount } = renderHook(() => useBrowserLocation()); const { result: state, unmount: unmountState } = renderHook(() => useHistoryState() ); const navigate = result.current[1]; act(() => navigate("/path", { state: { hello: "world" } })); expect(state.current).toStrictEqual({ hello: "world" }); unmount(); unmountState(); }); test("uses fail-safe escaping", () => { const { result } = renderHook(() => useBrowserLocation()); const navigate = result.current[1]; act(() => navigate("/%not-valid")); expect(result.current[0]).toBe("/%not-valid"); act(() => navigate("/99%")); expect(result.current[0]).toBe("/99%"); }); }); describe("`useSearch` hook", () => { beforeEach(() => history.replaceState(null, "", "/")); test("allows to get current search string", () => { const { result: searchResult } = renderHook(() => useSearch()); act(() => navigate("/foo?hello=world&whats=up")); expect(searchResult.current).toBe("?hello=world&whats=up"); }); test("returns empty string when there is no search string", () => { const { result: searchResult } = renderHook(() => useSearch()); expect(searchResult.current).toBe(""); act(() => navigate("/foo")); expect(searchResult.current).toBe(""); act(() => navigate("/foo? ")); expect(searchResult.current).toBe(""); }); test("does not re-render when only pathname is changed", () => { // count how many times each hook is rendered const locationRenders = { current: 0 }; const searchRenders = { current: 0 }; // count number of rerenders for each hook renderHook(() => { useEffect(() => { locationRenders.current += 1; }); return useBrowserLocation(); }); renderHook(() => { useEffect(() => { searchRenders.current += 1; }); return useSearch(); }); expect(locationRenders.current).toBe(1); expect(searchRenders.current).toBe(1); act(() => navigate("/foo")); expect(locationRenders.current).toBe(2); expect(searchRenders.current).toBe(1); act(() => navigate("/foo?bar")); expect(locationRenders.current).toBe(2); // no re-render expect(searchRenders.current).toBe(2); act(() => navigate("/baz?bar")); expect(locationRenders.current).toBe(3); // no re-render expect(searchRenders.current).toBe(2); }); }); describe("`update` second parameter", () => { test("rerenders the component", () => { const { result, unmount } = renderHook(() => useBrowserLocation()); const update = result.current[1]; act(() => update("/about")); expect(result.current[0]).toBe("/about"); unmount(); }); test("changes the current location", () => { const { result, unmount } = renderHook(() => useBrowserLocation()); const update = result.current[1]; act(() => update("/about")); expect(location.pathname).toBe("/about"); unmount(); }); test("saves a new entry in the History object", () => { const { result, unmount } = renderHook(() => useBrowserLocation()); const update = result.current[1]; const histBefore = history.length; act(() => update("/about")); expect(history.length).toBe(histBefore + 1); unmount(); }); test("replaces last entry with a new entry in the History object", () => { const { result, unmount } = renderHook(() => useBrowserLocation()); const update = result.current[1]; const histBefore = history.length; act(() => update("/foo", { replace: true })); expect(history.length).toBe(histBefore); expect(location.pathname).toBe("/foo"); unmount(); }); test("stays the same reference between re-renders (function ref)", () => { const { result, rerender, unmount } = renderHook(() => useBrowserLocation() ); const updateWas = result.current[1]; rerender(); const updateNow = result.current[1]; expect(updateWas).toBe(updateNow); unmount(); }); }); ================================================ FILE: packages/wouter/test/use-hash-location.test-d.ts ================================================ import { test, describe, expectTypeOf } from "bun:test"; import { useHashLocation, navigate } from "../src/use-hash-location.js"; import { BaseLocationHook } from "../src/index.js"; const assertType = (_value: T): void => {}; test("is a location hook", () => { expectTypeOf(useHashLocation).toMatchTypeOf(); expectTypeOf(useHashLocation()).toMatchTypeOf<[string, Function]>(); }); test("accepts a `ssrPath` path option", () => { useHashLocation({ ssrPath: "/foo" }); useHashLocation({ ssrPath: "" }); // @ts-expect-error useHashLocation({ base: 123 }); // @ts-expect-error useHashLocation({ unknown: "/base" }); }); describe("`navigate` function", () => { test("accepts an arbitrary `state` option", () => { navigate("/object", { state: { foo: "bar" } }); navigate("/symbol", { state: Symbol("foo") }); navigate("/string", { state: "foo" }); navigate("/undef", { state: undefined }); }); test("returns nothing", () => { assertType(navigate("/foo")); }); }); ================================================ FILE: packages/wouter/test/use-hash-location.test.tsx ================================================ import { test, expect, beforeEach, mock } from "bun:test"; import { renderHook, render, act } from "@testing-library/react"; import { renderToStaticMarkup } from "react-dom/server"; import { Router, Route, useLocation, Link } from "../src/index.js"; import { useHashLocation } from "../src/use-hash-location.js"; import { waitForHashChangeEvent } from "./test-utils"; import { ReactNode, useSyncExternalStore } from "react"; beforeEach(() => { history.replaceState(null, "", "/"); location.hash = ""; }); test("gets current location from `location.hash`", () => { location.hash = "/app/users"; const { result } = renderHook(() => useHashLocation()); const [path] = result.current; expect(path).toBe("/app/users"); }); test("isn't sensitive to leading slash", () => { location.hash = "app/users"; const { result } = renderHook(() => useHashLocation()); const [path] = result.current; expect(path).toBe("/app/users"); }); test("rerenders when hash changes", async () => { const { result } = renderHook(() => useHashLocation()); expect(result.current[0]).toBe("/"); await act(async () => { await waitForHashChangeEvent(() => { location.hash = "/app/users"; }); }); expect(result.current[0]).toBe("/app/users"); }); test("changes current hash when navigation is performed", () => { const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; act(() => { navigate("/app/users"); }); expect(location.hash).toBe("#/app/users"); }); test("should not rerender when pathname changes", () => { let renderCount = 0; location.hash = "/app"; const { result } = renderHook(() => { useHashLocation(); return ++renderCount; }); expect(result.current).toBe(1); history.replaceState(null, "", "/foo?bar#/app"); expect(result.current).toBe(1); }); test("does not change anything besides the hash when doesn't contain ? symbol", () => { history.replaceState(null, "", "/foo?bar#/app"); const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; act(() => { navigate("/settings/general"); }); expect(location.pathname).toBe("/foo"); expect(location.search).toBe("?bar"); }); test("changes search and hash when contains ? symbol", () => { history.replaceState(null, "", "/foo?bar#/app"); const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; act(() => { navigate("/abc?def"); }); expect(location.pathname).toBe("/foo"); expect(location.search).toBe("?def"); expect(location.hash).toBe("#/abc"); }); test("creates a new history entry when navigating", () => { const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; const initialLength = history.length; act(() => { navigate("/about"); }); expect(history.length).toBe(initialLength + 1); }); test("supports `state` option when navigating", () => { const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; act(() => { navigate("/app/users", { state: { hello: "world" } }); }); expect(history.state).toStrictEqual({ hello: "world" }); }); test("never changes reference to `navigate` between rerenders", () => { const { result, rerender } = renderHook(() => useHashLocation()); const updateWas = result.current[1]; rerender(); expect(result.current[1]).toBe(updateWas); }); test("uses `ssrPath` when rendered on the server", () => { const App = () => { const [path] = useHashLocation({ ssrPath: "/hello-from-server" }); return <>{path}; }; const rendered = renderToStaticMarkup(); expect(rendered).toBe("/hello-from-server"); }); test("is not sensitive to leading / or # when navigating", async () => { const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; await act(async () => { await waitForHashChangeEvent(() => navigate("look-ma-no-slashes")); }); expect(location.hash).toBe("#/look-ma-no-slashes"); expect(result.current[0]).toBe("/look-ma-no-slashes"); await act(async () => { await waitForHashChangeEvent(() => navigate("#/look-ma-no-hashes")); }); expect(location.hash).toBe("#/look-ma-no-hashes"); expect(result.current[0]).toBe("/look-ma-no-hashes"); }); test("works even if `hashchange` listeners are called asynchronously ", async () => { const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0)); // we want `hashchange` to stop invoking listeners before it reaches the // outer . this is done to simulate a situation when // `hashchange` listeners are called asynchrounously // // per https://github.com/whatwg/html/issues/1792 // some browsers fire `hashchange` and `popstate` asynchronously, so // when the event listeners are called, a microtask can be scheduled in between, // and we may end up with a teared state. inner components subscribe to `hashchange` // earlier so they may render even though their parent route does not match const subscribeToHashchange = (cb: () => void) => { const fn = (event: HashChangeEvent) => { event.stopImmediatePropagation(); cb(); }; window.addEventListener("hashchange", fn); return () => window.removeEventListener("hashchange", fn); }; const InterceptAndStopHashchange = ({ children, }: { children: ReactNode; }) => { useSyncExternalStore(subscribeToHashchange, () => true); return <>{children}; }; const paths: string[] = []; // keep track of rendered paths const LogLocations = () => { paths.push(useLocation()[0]); return null; }; act(() => { location.hash = "#/a"; }); const { unmount } = render( ); await act(async () => { location.hash = "#/b"; // wait for all `hashchange` listeners to be called // can't use `waitForHashChangeEvent` here because it gets cancelled along the way await nextTick(); }); // paths should not contain "b", because the outer route // does not match, so inner component should not be rendered expect(paths).toEqual(["/a"]); unmount(); }); test("defines a custom way of rendering link hrefs", () => { const { getByTestId } = render( ); expect(getByTestId("link")).toHaveAttribute("href", "#/app"); }); test("handles navigation with data: protocol", async () => { const originalHref = location.href; location.href = "data:text/html,content"; expect(location.protocol).toBe("data:"); const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; const initialHistoryLength = history.length; await waitForHashChangeEvent(() => { navigate("/new-path"); }); expect(location.hash).toBe("#/new-path"); expect(history.length).toBe(initialHistoryLength + 1); await waitForHashChangeEvent(() => { navigate("/another-path", { replace: true }); }); expect(location.hash).toBe("#/another-path"); expect(history.length).toBe(initialHistoryLength + 1); location.href = originalHref; }); test("interacts properly with the history stack", () => { const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; // case: replace, expect no history stack changes const historyStackCountBeforeReplace = history.length; act(() => { navigate("/app/users", { replace: true }); }); expect(location.hash).toBe("#/app/users"); expect(history.length).toBe(historyStackCountBeforeReplace); // case: push, expect history stack increase by 1 const historyStackCountBeforePush = history.length; act(() => { navigate("/app/users/2"); }); expect(location.hash).toBe("#/app/users/2"); expect(history.length).toBe(historyStackCountBeforePush + 1); }); test("dispatches hashchange event when options.replace is true", () => { const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; const hashChangeFn = mock(); addEventListener("hashchange", hashChangeFn); act(() => { navigate("/foo/bar", { replace: true }); }); expect(hashChangeFn).toBeCalled(); removeEventListener("hashchange", hashChangeFn); }); test("detects history change when navigate with options.replace is called", async () => { const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0)); const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; const newPath = "/foo/bar/baz"; act(() => { navigate(newPath, { replace: true }); }); await nextTick(); expect(result.current[0]).toBe(newPath); }); test("uses string URLs as hashchange event payload", () => { const { result } = renderHook(() => useHashLocation()); const [, navigate] = result.current; const relativeOldPath = "/foo"; const relativeNewPath = "/foo/bar/#hash"; const baseURL = "https://wouter.dev/#"; act(() => { navigate(relativeOldPath); }); let changeEvent = new HashChangeEvent("hashchange"); const hashChangeFn = (event: HashChangeEvent) => { changeEvent = event; }; addEventListener("hashchange", hashChangeFn); act(() => { navigate(relativeNewPath); }); expect(changeEvent?.newURL).toBe(`${baseURL}${relativeNewPath}`); expect(changeEvent?.oldURL).toBe(`${baseURL}${relativeOldPath}`); removeEventListener("hashchange", hashChangeFn); }); ================================================ FILE: packages/wouter/test/use-location.test.tsx ================================================ import { ComponentProps, ReactNode } from "react"; import { it, expect, describe, beforeEach } from "bun:test"; import { renderHook, act } from "@testing-library/react"; import { Router, useLocation } from "../src/index.js"; import { useBrowserLocation, navigate as browserNavigation, } from "../src/use-browser-location.js"; import { useHashLocation, navigate as hashNavigation, } from "../src/use-hash-location.js"; import { waitForHashChangeEvent } from "./test-utils"; import { memoryLocation } from "../src/memory-location.js"; function createContainer( options: Omit, "children"> = {} ) { return ({ children }: { children: ReactNode }) => ( {children} ); } const memory = memoryLocation({ record: true }); describe.each([ { name: "useBrowserLocation", hook: useBrowserLocation, location: () => location.pathname, navigate: browserNavigation, act, clear: () => { history.replaceState(null, "", "/"); }, }, { name: "useHashLocation", hook: useHashLocation, location: () => "/" + location.hash.replace(/^#?\/?/, ""), navigate: hashNavigation, act: (cb: () => void) => waitForHashChangeEvent(() => act(cb)), clear: () => { location.hash = ""; history.replaceState(null, "", "/"); }, }, { name: "memoryLocation", hook: memory.hook, location: () => memory.history.at(-1) ?? "", navigate: memory.navigate, act, clear: () => { memory.reset(); }, }, ])("$name", (stub) => { beforeEach(() => stub.clear()); it("returns a pair [value, update]", () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook }), }); const [value, update] = result.current; expect(typeof value).toBe("string"); expect(typeof update).toBe("function"); unmount(); }); describe("`value` first argument", () => { it("returns `/` when URL contains only a basepath", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/app", hook: stub.hook, }), }); await stub.act(() => stub.navigate("/app")); expect(result.current[0]).toBe("/"); unmount(); }); it("basepath should be case-insensitive", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/MyApp", hook: stub.hook, }), }); await stub.act(() => stub.navigate("/myAPP/users/JohnDoe")); expect(result.current[0]).toBe("/users/JohnDoe"); unmount(); }); it("returns an absolute path in case of unmatched base path", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/MyApp", hook: stub.hook, }), }); await stub.act(() => stub.navigate("/MyOtherApp/users/JohnDoe")); expect(result.current[0]).toBe("~/MyOtherApp/users/JohnDoe"); unmount(); }); it("automatically unescapes specials characters", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook, }), }); await stub.act(() => stub.navigate("/пользователи/показать все/101/げんきです") ); expect(result.current[0]).toBe( "/пользователи/показать все/101/げんきです" ); await stub.act(() => stub.navigate("/%D1%88%D0%B5%D0%BB%D0%BB%D1%8B")); expect(result.current[0]).toBe("/шеллы"); unmount(); }); it("can accept unescaped basepaths", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/hello мир", // basepath is not escaped hook: stub.hook, }), }); await stub.act(() => stub.navigate("/hello%20%D0%BC%D0%B8%D1%80/rel")); expect(result.current[0]).toBe("/rel"); unmount(); }); it("can accept unescaped basepaths", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/hello%20%D0%BC%D0%B8%D1%80", // basepath is already escaped hook: stub.hook, }), }); await stub.act(() => stub.navigate("/hello мир/rel")); expect(result.current[0]).toBe("/rel"); unmount(); }); }); describe("`update` second parameter", () => { it("rerenders the component", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook }), }); const update = result.current[1]; await stub.act(() => update("/about")); expect(stub.location()).toBe("/about"); unmount(); }); it("stays the same reference between re-renders (function ref)", () => { const { result, rerender, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ hook: stub.hook }), }); const updateWas = result.current[1]; rerender(); const updateNow = result.current[1]; expect(updateWas).toBe(updateNow); unmount(); }); it("supports a basepath", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/app", hook: stub.hook, }), }); const update = result.current[1]; await stub.act(() => update("/dashboard")); expect(stub.location()).toBe("/app/dashboard"); unmount(); }); it("ignores the '/' basepath", async () => { const { result, unmount } = renderHook(() => useLocation(), { wrapper: createContainer({ base: "/", hook: stub.hook, }), }); const update = result.current[1]; await stub.act(() => update("/dashboard")); expect(stub.location()).toBe("/dashboard"); unmount(); }); }); }); ================================================ FILE: packages/wouter/test/use-params.test-d.ts ================================================ import { test, expectTypeOf } from "bun:test"; import { useParams } from "../src/index.js"; test("does not accept any arguments", () => { expectTypeOf().parameters.toEqualTypeOf<[]>(); }); test("returns an object with arbitrary parameters", () => { const params = useParams(); expectTypeOf(params).toBeObject(); expectTypeOf(params.any).toEqualTypeOf(); expectTypeOf(params[0]).toEqualTypeOf(); }); test("can infer the type of parameters from the route path", () => { const params = useParams<"/app/users/:name?/:id">(); expectTypeOf(params).toMatchTypeOf<{ 0?: string; 1?: string; id: string; name?: string; }>(); }); test("can accept the custom type of parameters as a generic argument", () => { const params = useParams<{ foo: number; bar?: string }>(); expectTypeOf(params).toMatchTypeOf<{ foo: number; bar?: string; }>(); //@ts-expect-error return params.notFound; }); ================================================ FILE: packages/wouter/test/use-params.test.tsx ================================================ import { act, renderHook, cleanup } from "@testing-library/react"; import { test, expect, beforeEach, afterEach } from "bun:test"; import { useParams, Router, Route, Switch } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; beforeEach(() => history.replaceState(null, "", "/")); afterEach(cleanup); test("returns empty object when used outside of ", () => { const { result } = renderHook(() => useParams()); expect(result.current).toEqual({}); }); test("contains a * parameter when used inside an empty ", () => { const { result } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} ), }); expect(result.current).toEqual({ 0: "app-2/goods/tees", "*": "app-2/goods/tees", }); }); test("returns an empty object when there are no params", () => { const { result } = renderHook(() => useParams(), { wrapper: (props) => {props.children}, }); expect(result.current).toEqual({}); }); test("contains parameters from the closest parent ", () => { const { result } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} ), }); expect(result.current).toMatchObject({ 0: "1", 1: "maria", id: "1", name: "maria", }); }); test("inherits parameters from parent nested routes", () => { const { result } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} ), }); expect(result.current).toMatchObject({ name: "john", // name gets overriden "*": "summary-1", page: "dash", id: "10", // number params are overriden 0: "john", 1: "summary-1", }); }); test("rerenders with parameters change", () => { const { hook, navigate } = memoryLocation({ path: "/" }); const { result } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} ), }); expect(result.current).toBeNull(); act(() => navigate("/posts/all")); expect(result.current).toMatchObject({ 0: "posts", 1: "all", a: "posts", b: "all", }); act(() => navigate("/posts/latest")); expect(result.current).toMatchObject({ 0: "posts", 1: "latest", a: "posts", b: "latest", }); }); test("extracts parameters of the nested route", () => { const { hook } = memoryLocation({ path: "/v2/eth/txns", static: true, }); const { result } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} ), }); expect(result.current).toEqual({ 0: "v2", 1: "eth", version: "v2", chain: "eth", }); }); test("keeps the object ref the same if params haven't changed", () => { const { hook } = memoryLocation({ path: "/foo/bar" }); const { result, rerender } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} ), }); const firstRenderedParams = result.current; rerender(); expect(result.current).toBe(firstRenderedParams); }); test("works when the route becomes matching", () => { const { hook, navigate } = memoryLocation({ path: "/" }); const { result } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} ), }); act(() => navigate("/123")); expect(result.current).toMatchObject({ id: "123" }); }); test("makes the params an empty object, when there are no path params", () => { const { hook, navigate } = memoryLocation({ path: "/" }); const { result } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} {props.children} ), }); act(() => navigate("/posts/all")); act(() => navigate("/posts")); expect(Object.keys(result.current).length).toBe(0); }); test("removes route parameters when no longer present in the path", () => { // Start at a route that has both 'category' and 'page' in its params const { hook, navigate } = memoryLocation({ path: "/products/categories/apple/page/1", }); // Render useParams within two routes: one with /page/:page, one without const { result } = renderHook(() => useParams(), { wrapper: (props) => ( {props.children} {props.children} ), }); // Initial params should include 'category' and 'page' expect(result.current).toMatchObject({ 0: "apple", 1: "1", category: "apple", page: "1", }); // Navigate to a path that no longer contains the page param act(() => navigate("/products/categories/apple")); // The 'page' param should now be removed expect(result.current).toEqual({ 0: "apple", category: "apple", }); }); ================================================ FILE: packages/wouter/test/use-route.test-d.ts ================================================ import { test, expectTypeOf } from "bun:test"; import { useRoute } from "../src/index.js"; const assertType = (_value: T): void => {}; test("should only accept strings", () => { // @ts-expect-error assertType(useRoute(Symbol())); // @ts-expect-error assertType(useRoute()); assertType(useRoute("/")); }); test('has a boolean "match" result as a first returned value', () => { const [match] = useRoute("/"); expectTypeOf(match).toEqualTypeOf(); }); test("returns null as parameters when there was no match", () => { const [match, params] = useRoute("/foo"); if (!match) { expectTypeOf(params).toEqualTypeOf(); } }); test("accepts the type of parameters as a generic argument", () => { const [match, params] = useRoute<{ id: string; name: string | undefined }>( "/app/users/:name?/:id" ); if (match) { expectTypeOf(params).toEqualTypeOf<{ id: string; name: string | undefined; }>(); } }); test("infers parameters from the route path", () => { const [, inferedParams] = useRoute("/app/users/:name?/:id/*?"); if (inferedParams) { expectTypeOf(inferedParams).toMatchTypeOf<{ 0?: string; 1?: string; 2?: string; name?: string; id: string; wildcard?: string; }>(); } }); ================================================ FILE: packages/wouter/test/use-route.test.tsx ================================================ import { renderHook, act } from "@testing-library/react"; import { useRoute, Match, Router, RegexRouteParams } from "../src/index.js"; import { it, expect } from "bun:test"; import { memoryLocation } from "../src/memory-location.js"; it("is case insensitive", () => { assertRoute("/Users", "/users", {}); assertRoute("/HomePage", "/Homepage", {}); assertRoute("/Users/:Name", "/users/alex", { 0: "alex", Name: "alex" }); }); it("supports required segments", () => { assertRoute("/:page", "/users", { 0: "users", page: "users" }); assertRoute("/:page", "/users/all", false); assertRoute("/:page", "/1", { 0: "1", page: "1" }); assertRoute("/home/:page/etc", "/home/users/etc", { 0: "users", page: "users", }); assertRoute("/home/:page/etc", "/home/etc", false); assertRoute( "/root/payments/:id/refunds/:refId", "/root/payments/1/refunds/2", [true, { 0: "1", 1: "2", id: "1", refId: "2" }] ); }); it("ignores the trailing slash", () => { assertRoute("/home", "/home/", {}); assertRoute("/home", "/home", {}); assertRoute("/home/", "/home/", {}); assertRoute("/home/", "/home", {}); assertRoute("/:page", "/users/", [true, { 0: "users", page: "users" }]); assertRoute("/catalog/:section?", "/catalog/", { 0: undefined, section: undefined, }); }); it("supports trailing wildcards", () => { assertRoute("/app/*", "/app/", { 0: "", "*": "" }); assertRoute("/app/*", "/app/dashboard/intro", { 0: "dashboard/intro", "*": "dashboard/intro", }); assertRoute("/app/*", "/app/charges/1", { 0: "charges/1", "*": "charges/1" }); }); it("supports wildcards in the middle of the pattern", () => { assertRoute("/app/*/settings", "/app/users/settings", { 0: "users", "*": "users", }); assertRoute("/app/*/settings", "/app/users/1/settings", { 0: "users/1", "*": "users/1", }); assertRoute("/*/payments/:id", "/home/payments/1", { 0: "home", 1: "1", "*": "home", id: "1", }); assertRoute("/*/payments/:id?", "/home/payments", { 0: "home", 1: undefined, "*": "home", id: undefined, }); }); it("uses a question mark to define optional segments", () => { assertRoute("/books/:genre/:title?", "/books/scifi", { 0: "scifi", 1: undefined, genre: "scifi", title: undefined, }); assertRoute("/books/:genre/:title?", "/books/scifi/dune", { 0: "scifi", 1: "dune", genre: "scifi", title: "dune", }); assertRoute("/books/:genre/:title?", "/books/scifi/dune/all", false); assertRoute("/app/:company?/blog/:post", "/app/apple/blog/mac", { 0: "apple", 1: "mac", company: "apple", post: "mac", }); assertRoute("/app/:company?/blog/:post", "/app/blog/mac", { 0: undefined, 1: "mac", company: undefined, post: "mac", }); }); it("supports optional wildcards", () => { assertRoute("/app/*?", "/app/blog/mac", { 0: "blog/mac", "*": "blog/mac" }); assertRoute("/app/*?", "/app", { 0: undefined, "*": undefined }); assertRoute("/app/*?/dashboard", "/app/v1/dashboard", { 0: "v1", "*": "v1" }); assertRoute("/app/*?/dashboard", "/app/dashboard", { 0: undefined, "*": undefined, }); assertRoute("/app/*?/users/:name", "/app/users/karen", { 0: undefined, 1: "karen", "*": undefined, name: "karen", }); }); it("supports other characters in segments", () => { assertRoute("/users/:name", "/users/1-alex", { 0: "1-alex", name: "1-alex" }); assertRoute("/staff/:name/:bio?", "/staff/John Doe 3", { 0: "John Doe 3", 1: undefined, name: "John Doe 3", bio: undefined, }); assertRoute("/staff/:name/:bio?", "/staff/John Doe 3/bio", { 0: "John Doe 3", 1: "bio", name: "John Doe 3", bio: "bio", }); assertRoute("/users/:name/bio", "/users/$102_Kathrine&/bio", { 0: "$102_Kathrine&", name: "$102_Kathrine&", }); }); it("ignores escaped slashes", () => { assertRoute("/:param/bar", "/foo%2Fbar/bar", { 0: "foo%2Fbar", param: "foo%2Fbar", }); assertRoute("/:param", "/foo%2Fbar%D1%81%D0%B0%D0%BD%D1%8F", { 0: "foo%2Fbarсаня", param: "foo%2Fbarсаня", }); }); it("supports regex patterns", () => { assertRoute(/[/]foo/, "/foo", {}); assertRoute(/[/]([a-z]+)/, "/bar", { 0: "bar" }); assertRoute(/[/]([a-z]+)/, "/123", false); assertRoute(/[/](?[a-z]+)/, "/bar", { 0: "bar", param: "bar" }); assertRoute(/[/](?[a-z]+)/, "/123", false); }); it("reacts to pattern updates", () => { const { result, rerender } = renderHook( ({ pattern }: { pattern: string }) => useRoute(pattern), { wrapper: (props) => ( ), initialProps: { pattern: "/" }, } ); expect(result.current).toStrictEqual([false, null]); rerender({ pattern: "/blog/:category/:post/:action" }); expect(result.current).toStrictEqual([ true, { 0: "products", 1: "40", 2: "read-all", category: "products", post: "40", action: "read-all", } as any, ]); rerender({ pattern: "/blog/products/:id?/read-all" }); expect(result.current).toStrictEqual([true, { 0: "40", id: "40" } as any]); rerender({ pattern: "/blog/products/:name" }); expect(result.current).toStrictEqual([false, null]); rerender({ pattern: "/blog/*" }); expect(result.current).toStrictEqual([ true, { 0: "products/40/read-all", "*": "products/40/read-all" } as any, ]); }); it("reacts to location updates", () => { const { hook, navigate } = memoryLocation(); const { result } = renderHook(() => useRoute("/cities/:city?"), { wrapper: (props) => , }); expect(result.current).toStrictEqual([false, null]); act(() => navigate("/cities/berlin")); expect(result.current).toStrictEqual([true, { 0: "berlin", city: "berlin" }]); act(() => navigate("/cities/Tokyo")); expect(result.current).toStrictEqual([true, { 0: "Tokyo", city: "Tokyo" }]); act(() => navigate("/about")); expect(result.current).toStrictEqual([false, null]); act(() => navigate("/cities")); expect(result.current).toStrictEqual([ true, { 0: undefined, city: undefined }, ]); }); /** * Assertion helper to test useRoute() return values. */ const assertRoute = ( pattern: string | RegExp, location: string, rhs: false | Match | Record ) => { const { result } = renderHook(() => useRoute(pattern), { wrapper: (props) => ( ), }); if (rhs === false) { expect(result.current).toStrictEqual([false, null]); } else if (Array.isArray(rhs)) { expect(result.current).toStrictEqual(rhs); } else { expect(result.current).toStrictEqual([true, rhs]); } }; ================================================ FILE: packages/wouter/test/use-search-params.test.tsx ================================================ import { renderHook, act } from "@testing-library/react"; import { useSearchParams, Router } from "../src/index.js"; import { navigate } from "../src/use-browser-location.js"; import { it, expect, beforeEach } from "bun:test"; beforeEach(() => history.replaceState(null, "", "/")); it("can return browser search params", () => { history.replaceState(null, "", "/users?active=true"); const { result } = renderHook(() => useSearchParams()); expect(result.current[0].get("active")).toBe("true"); }); it("can change browser search params", () => { history.replaceState(null, "", "/users?active=true"); const { result } = renderHook(() => useSearchParams()); expect(result.current[0].get("active")).toBe("true"); act(() => result.current[1]((prev) => { prev.set("active", "false"); return prev; }) ); expect(result.current[0].get("active")).toBe("false"); }); it("can be customized in the Router", () => { const customSearchHook = ({ customOption = "unused" }) => "none"; const { result } = renderHook(() => useSearchParams(), { wrapper: (props) => { return {props.children}; }, }); expect(Array.from(result.current[0].keys())).toEqual(["none"]); }); it("unescapes search string", () => { const { result: searchResult } = renderHook(() => useSearchParams()); expect(Array.from(searchResult.current[0].keys()).length).toBe(0); act(() => navigate("/?nonce=not Found&country=საქართველო")); expect(searchResult.current[0].get("nonce")).toBe("not Found"); expect(searchResult.current[0].get("country")).toBe("საქართველო"); // question marks act(() => navigate("/?вопрос=как дела?")); expect(searchResult.current[0].get("вопрос")).toBe("как дела?"); }); it("is safe against parameter injection", () => { history.replaceState(null, "", "/?search=foo%26parameter_injection%3Dbar"); const { result } = renderHook(() => useSearchParams()); expect(result.current[0].get("search")).toBe("foo¶meter_injection=bar"); }); ================================================ FILE: packages/wouter/test/use-search.test.tsx ================================================ import { renderHook, act, cleanup } from "@testing-library/react"; import { useSearch, Router } from "../src/index.js"; import { navigate } from "../src/use-browser-location.js"; import { memoryLocation } from "../src/memory-location.js"; import { test, expect, beforeEach, afterEach } from "bun:test"; beforeEach(() => history.replaceState(null, "", "/")); afterEach(cleanup); test("returns browser search string", () => { history.replaceState(null, "", "/users?active=true"); const { result } = renderHook(() => useSearch()); expect(result.current).toEqual("active=true"); }); test("can be customized in the Router", () => { const customSearchHook = ({ customOption = "unused" }) => "none"; const { result } = renderHook(() => useSearch(), { wrapper: (props) => { return {props.children}; }, }); expect(result.current).toEqual("none"); }); test("can be customized with memoryLocation", () => { const { searchHook } = memoryLocation({ path: "/foo?key=value" }); const { result } = renderHook(() => useSearch(), { wrapper: (props) => { return {props.children}; }, }); expect(result.current).toEqual("key=value"); }); test("can be customized with memoryLocation using search path parameter", () => { const { searchHook } = memoryLocation({ path: "/foo?key=value", searchPath: "foo=bar", }); const { result } = renderHook(() => useSearch(), { wrapper: (props) => { return {props.children}; }, }); expect(result.current).toEqual("key=value&foo=bar"); }); test("auto-inherits searchHook from hook when not explicitly provided", () => { const { hook } = memoryLocation({ path: "/foo?key=value" }); const { result } = renderHook(() => useSearch(), { wrapper: (props) => { // Only pass hook, not searchHook - it should auto-inherit! return {props.children}; }, }); expect(result.current).toEqual("key=value"); }); test("unescapes search string", () => { const { result: searchResult } = renderHook(() => useSearch()); expect(searchResult.current).toBe(""); act(() => navigate("/?nonce=not Found&country=საქართველო")); expect(searchResult.current).toBe("nonce=not Found&country=საქართველო"); // question marks act(() => navigate("/?вопрос=как дела?")); expect(searchResult.current).toBe("вопрос=как дела?"); }); test("is safe against parameter injection", () => { history.replaceState(null, "", "/?search=foo%26parameter_injection%3Dbar"); const { result } = renderHook(() => useSearch()); const searchParams = new URLSearchParams(result.current); const query = Object.fromEntries(searchParams.entries()); expect(query).toEqual({ search: "foo¶meter_injection=bar" }); }); ================================================ FILE: packages/wouter/test/view-transitions.test.tsx ================================================ import { test, expect, describe, mock, afterEach } from "bun:test"; import { render, cleanup, fireEvent } from "@testing-library/react"; import { Router, Link, useLocation, type AroundNavHandler } from "../src/index.js"; import { memoryLocation } from "../src/memory-location.js"; afterEach(cleanup); describe("view transitions", () => { test("Link with transition prop triggers aroundNav with transition in options", () => { // 1. Setup: create aroundNav callback that captures calls const aroundNav: AroundNavHandler = mock((navigate, to, options) => { navigate(to, options); }); const { hook } = memoryLocation({ path: "/" }); // 2. Render Link with transition prop const { getByTestId } = render( About ); // 3. Click the link fireEvent.click(getByTestId("link")); // 4. Verify aroundNav was called with transition: true in options expect(aroundNav).toHaveBeenCalledTimes(1); const [navigateFn, to, options] = (aroundNav as ReturnType) .mock.calls[0]; expect(typeof navigateFn).toBe("function"); expect(to).toBe("/about"); expect(options.transition).toBe(true); }); test("useLocation navigate with transition option triggers aroundNav", () => { const aroundNav: AroundNavHandler = mock((navigate, to, options) => { navigate(to, options); }); const { hook } = memoryLocation({ path: "/" }); const NavigateButton = () => { const [, navigate] = useLocation(); return ( ); }; const { getByTestId } = render( ); fireEvent.click(getByTestId("btn")); expect(aroundNav).toHaveBeenCalledTimes(1); const [, to, options] = (aroundNav as ReturnType).mock.calls[0]; expect(to).toBe("/about"); expect(options.transition).toBe(true); }); test("navigation does not happen if aroundNav doesn't call navigate", () => { // aroundNav that does nothing const aroundNav: AroundNavHandler = mock(() => {}); const { hook } = memoryLocation({ path: "/" }); const LocationDisplay = () => { const [location] = useLocation(); return {location}; }; const { getByTestId } = render( About ); // Verify initial location expect(getByTestId("location").textContent).toBe("/"); // Click the link fireEvent.click(getByTestId("link")); // aroundNav was called but didn't call navigate expect(aroundNav).toHaveBeenCalledTimes(1); // Location should remain unchanged expect(getByTestId("location").textContent).toBe("/"); }); }); ================================================ FILE: packages/wouter/types/index.d.ts ================================================ // Minimum TypeScript Version: 4.1 // tslint:disable:no-unnecessary-generics import { AnchorHTMLAttributes, FunctionComponent, RefAttributes, ComponentType, ReactNode, ReactElement, MouseEventHandler, JSXElementConstructor, } from "react"; import { Path, PathPattern, BaseLocationHook, HookReturnValue, HookNavigationOptions, BaseSearchHook, } from "./location-hook.js"; import { BrowserLocationHook, BrowserSearchHook, } from "./use-browser-location.js"; import { Parser, RouterObject, RouterOptions } from "./router.js"; // these files only export types, so we can re-export them as-is // in TS 5.0 we'll be able to use `export type * from ...` export * from "./location-hook.js"; export * from "./router.js"; import { RouteParams } from "regexparam"; export type StringRouteParams = RouteParams & { [param: number]: string | undefined; }; export type RegexRouteParams = { [key: string | number]: string | undefined }; /** * Route patterns and parameters */ export interface DefaultParams { readonly [paramName: string | number]: string | undefined; } export type Params = T; export type MatchWithParams = [ true, Params ]; export type NoMatch = [false, null]; export type Match = | MatchWithParams | NoMatch; /* * Components: */ export interface RouteComponentProps { params: T; } export interface RouteProps< T extends DefaultParams | undefined = undefined, RoutePath extends PathPattern = PathPattern > { children?: | (( params: T extends DefaultParams ? T : RoutePath extends string ? StringRouteParams : RegexRouteParams ) => ReactNode) | ReactNode; path?: RoutePath; component?: JSXElementConstructor< RouteComponentProps< T extends DefaultParams ? T : RoutePath extends string ? StringRouteParams : RegexRouteParams > >; nest?: boolean; } export function Route< T extends DefaultParams | undefined = undefined, RoutePath extends PathPattern = PathPattern >(props: RouteProps): ReturnType; /* * Components: & */ export type NavigationalProps< H extends BaseLocationHook = BrowserLocationHook > = ({ to: Path; href?: never } | { href: Path; to?: never }) & HookNavigationOptions; export type RedirectProps = NavigationalProps & { children?: never; }; export function Redirect( props: RedirectProps, context?: any ): null; type AsChildProps = | ({ asChild?: false } & DefaultElementProps) | ({ asChild: true } & ComponentProps); type HTMLLinkAttributes = Omit< AnchorHTMLAttributes, "className" > & { className?: string | undefined | ((isActive: boolean) => string | undefined); }; export type LinkProps = NavigationalProps & AsChildProps< { children: ReactElement; onClick?: MouseEventHandler }, HTMLLinkAttributes & RefAttributes >; export function Link( props: LinkProps, context?: any ): ReturnType; /* * Components: */ export interface SwitchProps { location?: string; children: ReactNode; } export const Switch: FunctionComponent; /* * Components: */ export type RouterProps = RouterOptions & { children: ReactNode; }; export const Router: FunctionComponent; /* * Hooks */ export function useRouter(): RouterObject; export function useRoute< T extends DefaultParams | undefined = undefined, RoutePath extends PathPattern = PathPattern >( pattern: RoutePath ): Match< T extends DefaultParams ? T : RoutePath extends string ? StringRouteParams : RegexRouteParams >; export function useLocation< H extends BaseLocationHook = BrowserLocationHook >(): HookReturnValue; export function useSearch< H extends BaseSearchHook = BrowserSearchHook >(): ReturnType; export type URLSearchParamsInit = ConstructorParameters< typeof URLSearchParams >[0]; export type SetSearchParams = ( nextInit: | URLSearchParamsInit | ((prev: URLSearchParams) => URLSearchParamsInit), options?: { replace?: boolean; state?: any } ) => void; export function useSearchParams(): [URLSearchParams, SetSearchParams]; export function useParams(): T extends string ? StringRouteParams : T extends undefined ? DefaultParams : T; /* * Helpers */ export function matchRoute< T extends DefaultParams | undefined = undefined, RoutePath extends PathPattern = PathPattern >( parser: Parser, pattern: RoutePath, path: string, loose?: boolean ): Match< T extends DefaultParams ? T : RoutePath extends string ? StringRouteParams : RegexRouteParams >; // tslint:enable:no-unnecessary-generics ================================================ FILE: packages/wouter/types/location-hook.d.ts ================================================ /* * Foundation: useLocation and paths */ export type Path = string; export type PathPattern = string | RegExp; export type SearchString = string; export type HrefsFormatter = (href: string, router?: any) => string; // the base useLocation hook type. Any custom hook (including the // default one) should inherit from it. export type BaseLocationHook = { (...args: any[]): [Path, (path: Path, ...args: any[]) => any]; searchHook?: BaseSearchHook; hrefs?: HrefsFormatter; }; export type BaseSearchHook = (...args: any[]) => SearchString; /* * Utility types that operate on hook */ // Returns the type of the location tuple of the given hook. export type HookReturnValue = ReturnType; // Utility type that allows us to handle cases like `any` and `never` type EmptyInterfaceWhenAnyOrNever = 0 extends 1 & T ? {} : [T] extends [never] ? {} : T; // Returns the type of the navigation options that hook's push function accepts. export type HookNavigationOptions = EmptyInterfaceWhenAnyOrNever< NonNullable[1]>[1]> // get's the second argument of a tuple returned by the hook >; ================================================ FILE: packages/wouter/types/memory-location.d.ts ================================================ import { BaseLocationHook, BaseSearchHook, Path, SearchString, } from "./location-hook.js"; type Navigate = ( to: Path, options?: { replace?: boolean; state?: S; transition?: boolean } ) => void; type HookReturnValue = { hook: BaseLocationHook; searchHook: BaseSearchHook; navigate: Navigate; }; type StubHistory = { history: Path[]; reset: () => void }; export function memoryLocation(options?: { path?: Path; searchPath?: SearchString; static?: boolean; record?: false; }): HookReturnValue; export function memoryLocation(options?: { path?: Path; searchPath?: SearchString; static?: boolean; record: true; }): HookReturnValue & StubHistory; ================================================ FILE: packages/wouter/types/router.d.ts ================================================ import { Path, SearchString, BaseLocationHook, BaseSearchHook, HrefsFormatter, } from "./location-hook.js"; export type Parser = ( route: Path, loose?: boolean ) => { pattern: RegExp; keys: string[] }; // Standard navigation options supported by all built-in location hooks export type NavigateOptions = { replace?: boolean; state?: S; /** Enable view transitions for this navigation (used with aroundNav) */ transition?: boolean; }; // Function that wraps navigate calls, useful for view transitions export type AroundNavHandler = ( navigate: (to: Path, options?: NavigateOptions) => void, to: Path, options?: NavigateOptions ) => void; // the object returned from `useRouter` export interface RouterObject { readonly hook: BaseLocationHook; readonly searchHook: BaseSearchHook; readonly base: Path; readonly ownBase: Path; readonly parser: Parser; readonly ssrPath?: Path; readonly ssrSearch?: SearchString; readonly ssrContext?: SsrContext; readonly hrefs: HrefsFormatter; readonly aroundNav: AroundNavHandler; } // state captured during SSR render export type SsrContext = { // if a redirect was encountered, this will be populated with the path redirectTo?: Path; // HTTP status code to set for SSR response statusCode?: number; }; // basic options to construct a router export type RouterOptions = { hook?: BaseLocationHook; searchHook?: BaseSearchHook; base?: Path; parser?: Parser; ssrPath?: Path; ssrSearch?: SearchString; ssrContext?: SsrContext; hrefs?: HrefsFormatter; aroundNav?: AroundNavHandler; }; ================================================ FILE: packages/wouter/types/use-browser-location.d.ts ================================================ import { Path, SearchString } from "./location-hook.js"; type Primitive = string | number | bigint | boolean | null | undefined | symbol; export const useLocationProperty: ( fn: () => S, ssrFn?: () => S ) => S; export type BrowserSearchHook = (options?: { ssrSearch?: SearchString; }) => SearchString; export const useSearch: BrowserSearchHook; export const usePathname: (options?: { ssrPath?: Path }) => Path; export const useHistoryState: () => T; export const navigate: ( to: string | URL, options?: { replace?: boolean; state?: S; transition?: boolean } ) => void; /* * Default `useLocation` */ // The type of the default `useLocation` hook that wouter uses. // It operates on current URL using History API, supports base path and can // navigate with `pushState` or `replaceState`. export type BrowserLocationHook = (options?: { ssrPath?: Path; }) => [Path, typeof navigate]; export const useBrowserLocation: BrowserLocationHook; ================================================ FILE: packages/wouter/types/use-hash-location.d.ts ================================================ import { Path } from "./location-hook.js"; export function navigate( to: Path, options?: { state?: S; replace?: boolean; transition?: boolean } ): void; export function useHashLocation(options?: { ssrPath?: Path; }): [Path, typeof navigate]; ================================================ FILE: packages/wouter-preact/.gitignore ================================================ # Copied from wouter during prepublish (react-deps.js stays as the Preact version) src/index.js src/memory-location.js src/paths.js src/use-browser-location.js src/use-hash-location.js src/use-sync-external-store.js src/use-sync-external-store.native.js ================================================ FILE: packages/wouter-preact/package.json ================================================ { "name": "wouter-preact", "version": "3.9.0", "description": "Minimalist-friendly ~1.5KB router for Preact", "type": "module", "keywords": [ "react", "preact", "router", "tiny", "routing", "hooks", "useLocation" ], "files": [ "src", "types/**/*.d.ts", "types/*.d.ts" ], "main": "src/index.js", "exports": { ".": { "types": "./types/index.d.ts", "default": "./src/index.js" }, "./use-browser-location": { "types": "./types/use-browser-location.d.ts", "default": "./src/use-browser-location.js" }, "./use-hash-location": { "types": "./types/use-hash-location.d.ts", "default": "./src/use-hash-location.js" }, "./memory-location": { "types": "./types/memory-location.d.ts", "default": "./src/memory-location.js" } }, "types": "types/index.d.ts", "typesVersions": { ">=4.1": { "types/index.d.ts": [ "types/index.d.ts" ], "use-browser-location": [ "types/use-browser-location.d.ts" ], "use-hash-location": [ "types/use-hash-location.d.ts" ], "memory-location": [ "types/memory-location.d.ts" ] } }, "scripts": { "prepublishOnly": "copyfiles -f ../wouter/src/index.js ../wouter/src/memory-location.js ../wouter/src/paths.js ../wouter/src/use-browser-location.js ../wouter/src/use-hash-location.js ../wouter/src/use-sync-external-store.js ../wouter/src/use-sync-external-store.native.js src && cp ../../README.md ." }, "author": "Alexey Taktarov ", "repository": { "type": "git", "url": "git+https://github.com/molefrog/wouter.git" }, "license": "Unlicense", "peerDependencies": { "preact": "^10.0.0" }, "dependencies": { "mitt": "^3.0.1", "regexparam": "^3.0.0" }, "devDependencies": { "wouter": "*" } } ================================================ FILE: packages/wouter-preact/src/react-deps.js ================================================ import { useState, useLayoutEffect, useEffect, useRef } from "preact/hooks"; export { isValidElement, createContext, cloneElement, createElement, Fragment, } from "preact"; export { useMemo, useRef, useLayoutEffect as useIsomorphicLayoutEffect, useLayoutEffect as useInsertionEffect, useState, useContext, } from "preact/hooks"; // Copied from: // https://github.com/facebook/react/blob/main/packages/shared/ExecutionEnvironment.js const canUseDOM = !!( typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined" ); // TODO: switch to `export { useSyncExternalStore } from "preact/compat"` once we update Preact to >= 10.11.3 function is(x, y) { return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); } export function useSyncExternalStore(subscribe, getSnapshot, getSSRSnapshot) { if (getSSRSnapshot && !canUseDOM) getSnapshot = getSSRSnapshot; const value = getSnapshot(); const [{ _instance }, forceUpdate] = useState({ _instance: { _value: value, _getSnapshot: getSnapshot }, }); useLayoutEffect(() => { _instance._value = value; _instance._getSnapshot = getSnapshot; if (!is(_instance._value, getSnapshot())) { forceUpdate({ _instance }); } }, [subscribe, value, getSnapshot]); useEffect(() => { if (!is(_instance._value, _instance._getSnapshot())) { forceUpdate({ _instance }); } return subscribe(() => { if (!is(_instance._value, _instance._getSnapshot())) { forceUpdate({ _instance }); } }); }, [subscribe]); return value; } // provide forwardRef stub for preact export function forwardRef(component) { return component; } // Userland polyfill while we wait for the forthcoming // https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md // Note: "A high-fidelty polyfill for useEvent is not possible because // there is no lifecycle or Hook in React that we can use to switch // .current at the right timing." // So we will have to make do with this "close enough" approach for now. export const useEvent = (fn) => { const ref = useRef([fn, (...args) => ref[0](...args)]).current; useLayoutEffect(() => { ref[0] = fn; }); return ref[1]; }; ================================================ FILE: packages/wouter-preact/test/preact.test.tsx ================================================ /** @jsx h */ /** @jsxFrag Fragment */ /** @jsxImportSource preact */ import { test, expect, describe, beforeEach, afterEach, beforeAll, afterAll, mock, } from "bun:test"; import { render } from "preact"; import { act, setupRerender, teardown } from "preact/test-utils"; import renderToString from "preact-render-to-string"; import { copyFile, rm } from "fs/promises"; import { join } from "path"; import type * as WouterPreact from "../types/index.js"; const assertType = (_value: T): void => {}; // Files to copy from wouter/src to wouter-preact/src const filesToCopy = [ "memory-location.js", "paths.js", "use-browser-location.js", "use-hash-location.js", "use-sync-external-store.js", "use-sync-external-store.native.js", "index.js", ]; async function loadPreact(): Promise { // Import from the copied files in src/ directory const module = (await import( join(import.meta.dir, "../src/index.js") )) as typeof WouterPreact; return module; } beforeAll(async () => { const wouterSrc = join(import.meta.dir, "../../wouter/src"); const preactSrc = join(import.meta.dir, "../src"); for (const file of filesToCopy) { await copyFile(join(wouterSrc, file), join(preactSrc, file)); } }); afterAll(async () => { const preactSrc = join(import.meta.dir, "../src"); for (const file of filesToCopy) { await rm(join(preactSrc, file), { force: true }); } }); describe("Preact support", () => { beforeEach(() => { setupRerender(); }); afterEach(() => { teardown(); }); describe("useRoute", () => { test("should only accept strings", async () => { const { useRoute } = await loadPreact(); const Component = () => { // @ts-expect-error assertType(useRoute(Symbol())); // @ts-expect-error assertType(useRoute()); assertType(useRoute("/")); return
    Hello
    ; }; expect(typeof Component).toBe("function"); // dummy, we only care about the types }); }); test("renders properly and reacts on navigation", async () => { const { Route, Link, Switch } = await loadPreact(); const container = document.body.appendChild(document.createElement("div")); const fn = mock(); const App = () => { const handleAsChildClick = mock(); return ( <>
    <>Welcome to the list of {100} greatest albums of all time! Rolling Stones Best 100 Albums {(params) => `Album ${params.name}`} Nothing was found!
    ); }; let node = render(, container); const routesEl = container.querySelector('[data-testid="routes"]')!; const indexLinkEl = container.querySelector('[data-testid="index-link"]')!; const featLinkEl = container.querySelector( '[data-testid="featured-link"]' )!; // default route should be rendered expect(routesEl.textContent).toBe("Nothing was found!"); expect(featLinkEl.getAttribute("href")).toBe("/albums/london-calling"); // link renders as A element expect(indexLinkEl.tagName).toBe("A"); act(() => { const evt = new MouseEvent("click", { bubbles: true, cancelable: true, button: 0, }); indexLinkEl.dispatchEvent(evt); }); // performs a navigation when the link is clicked expect(location.pathname).toBe("/albums/all"); // Link accepts an `onClick` prop, fired after the navigation expect(fn).toHaveBeenCalledTimes(1); }); }); describe("Preact SSR", () => { test.skip("supports SSR (fix: useSyncExternalStore polyfill in Bun)", async () => { const { Router, useLocation } = await loadPreact(); const LocationPrinter = () => { const [location] = useLocation(); return <>location = {location}; }; const rendered = renderToString( ); expect(rendered).toContain("/ssr/preact"); }); }); ================================================ FILE: packages/wouter-preact/types/index.d.ts ================================================ // Minimum TypeScript Version: 4.1 // tslint:disable:no-unnecessary-generics import { JSX, FunctionComponent, ComponentType, ComponentChildren, } from "preact"; import { Path, PathPattern, BaseLocationHook, HookReturnValue, HookNavigationOptions, BaseSearchHook, } from "./location-hook.js"; import { BrowserLocationHook, BrowserSearchHook, } from "./use-browser-location.js"; import { RouterObject, RouterOptions, Parser } from "./router.js"; // these files only export types, so we can re-export them as-is // in TS 5.0 we'll be able to use `export type * from ...` export * from "./location-hook.js"; export * from "./router.js"; import { RouteParams } from "regexparam"; export type StringRouteParams = RouteParams & { [param: number]: string | undefined; }; export type RegexRouteParams = { [key: string | number]: string | undefined }; /** * Route patterns and parameters */ export interface DefaultParams { readonly [paramName: string | number]: string | undefined; } export type Params = T; export type MatchWithParams = [ true, Params ]; export type NoMatch = [false, null]; export type Match = | MatchWithParams | NoMatch; /* * Components: */ export interface RouteComponentProps { params: T; } export interface RouteProps< T extends DefaultParams | undefined = undefined, RoutePath extends PathPattern = PathPattern > { children?: | (( params: T extends DefaultParams ? T : RoutePath extends string ? StringRouteParams : RegexRouteParams ) => ComponentChildren) | ComponentChildren; path?: RoutePath; component?: ComponentType< RouteComponentProps< T extends DefaultParams ? T : RoutePath extends string ? StringRouteParams : RegexRouteParams > >; nest?: boolean; } export function Route< T extends DefaultParams | undefined = undefined, RoutePath extends PathPattern = PathPattern >(props: RouteProps): ReturnType; /* * Components: & */ export type NavigationalProps< H extends BaseLocationHook = BrowserLocationHook > = ({ to: Path; href?: never } | { href: Path; to?: never }) & HookNavigationOptions; type AsChildProps = | ({ asChild?: false } & DefaultElementProps) | ({ asChild: true } & ComponentProps); type HTMLLinkAttributes = Omit & { className?: string | undefined | ((isActive: boolean) => string | undefined); }; export type LinkProps = NavigationalProps & AsChildProps< { children: ComponentChildren; onClick?: JSX.MouseEventHandler }, HTMLLinkAttributes >; export type RedirectProps = NavigationalProps & { children?: never; }; export function Redirect( props: RedirectProps, context?: any ): null; export function Link( props: LinkProps, context?: any ): ReturnType; /* * Components: */ export interface SwitchProps { location?: string; children: ComponentChildren; } export const Switch: FunctionComponent; /* * Components: */ export type RouterProps = RouterOptions & { children: ComponentChildren; }; export const Router: FunctionComponent; /* * Hooks */ export function useRouter(): RouterObject; export function useRoute< T extends DefaultParams | undefined = undefined, RoutePath extends PathPattern = PathPattern >( pattern: RoutePath ): Match< T extends DefaultParams ? T : RoutePath extends string ? StringRouteParams : RegexRouteParams >; export function useLocation< H extends BaseLocationHook = BrowserLocationHook >(): HookReturnValue; export function useSearch< H extends BaseSearchHook = BrowserSearchHook >(): ReturnType; export type URLSearchParamsInit = ConstructorParameters< typeof URLSearchParams >[0]; export type SetSearchParams = ( nextInit: | URLSearchParamsInit | ((prev: URLSearchParams) => URLSearchParamsInit), options?: { replace?: boolean; state?: any } ) => void; export function useSearchParams(): [URLSearchParams, SetSearchParams]; export function useParams(): T extends string ? StringRouteParams : T extends undefined ? DefaultParams : T; /* * Helpers */ export function matchRoute< T extends DefaultParams | undefined = undefined, RoutePath extends PathPattern = PathPattern >( parser: Parser, pattern: RoutePath, path: string, loose?: boolean ): Match< T extends DefaultParams ? T : RoutePath extends string ? StringRouteParams : RegexRouteParams >; // tslint:enable:no-unnecessary-generics ================================================ FILE: packages/wouter-preact/types/location-hook.d.ts ================================================ /* * Foundation: useLocation and paths */ export type Path = string; export type PathPattern = string | RegExp; export type SearchString = string; export type HrefsFormatter = (href: string, router?: any) => string; // the base useLocation hook type. Any custom hook (including the // default one) should inherit from it. export type BaseLocationHook = { (...args: any[]): [Path, (path: Path, ...args: any[]) => any]; searchHook?: BaseSearchHook; hrefs?: HrefsFormatter; }; export type BaseSearchHook = (...args: any[]) => SearchString; /* * Utility types that operate on hook */ // Returns the type of the location tuple of the given hook. export type HookReturnValue = ReturnType; // Returns the type of the navigation options that hook's push function accepts. export type HookNavigationOptions = HookReturnValue[1] extends ( path: Path, options: infer R, ...rest: any[] ) => any ? R extends { [k: string]: any } ? R : {} : {}; ================================================ FILE: packages/wouter-preact/types/memory-location.d.ts ================================================ import { BaseLocationHook, Path } from "./location-hook.js"; type Navigate = ( to: Path, options?: { replace?: boolean; state?: S; transition?: boolean } ) => void; type HookReturnValue = { hook: BaseLocationHook; navigate: Navigate }; type StubHistory = { history: Path[]; reset: () => void }; export function memoryLocation(options?: { path?: Path; static?: boolean; record?: false; }): HookReturnValue; export function memoryLocation(options?: { path?: Path; static?: boolean; record: true; }): HookReturnValue & StubHistory; ================================================ FILE: packages/wouter-preact/types/router.d.ts ================================================ import { Path, SearchString, BaseLocationHook, BaseSearchHook, HrefsFormatter, } from "./location-hook.js"; export type Parser = ( route: Path, loose?: boolean ) => { pattern: RegExp; keys: string[] }; // Standard navigation options supported by all built-in location hooks export type NavigateOptions = { replace?: boolean; state?: S; /** Enable view transitions for this navigation (used with aroundNav) */ transition?: boolean; }; // Function that wraps navigate calls, useful for view transitions export type AroundNavHandler = ( navigate: (to: Path, options?: NavigateOptions) => void, to: Path, options?: NavigateOptions ) => void; // the object returned from `useRouter` export interface RouterObject { readonly hook: BaseLocationHook; readonly searchHook: BaseSearchHook; readonly base: Path; readonly ownBase: Path; readonly parser: Parser; readonly ssrPath?: Path; readonly ssrSearch?: SearchString; readonly ssrContext?: SsrContext; readonly hrefs: HrefsFormatter; readonly aroundNav: AroundNavHandler; } // state captured during SSR render export type SsrContext = { // if a redirect was encountered, this will be populated with the path redirectTo?: Path; // HTTP status code to set for SSR response statusCode?: number; }; // basic options to construct a router export type RouterOptions = { hook?: BaseLocationHook; searchHook?: BaseSearchHook; base?: Path; parser?: Parser; ssrPath?: Path; ssrSearch?: SearchString; ssrContext?: SsrContext; hrefs?: HrefsFormatter; aroundNav?: AroundNavHandler; }; ================================================ FILE: packages/wouter-preact/types/use-browser-location.d.ts ================================================ import { Path, SearchString } from "./location-hook.js"; type Primitive = string | number | bigint | boolean | null | undefined | symbol; export const useLocationProperty: ( fn: () => S, ssrFn?: () => S ) => S; export type BrowserSearchHook = (options?: { ssrSearch?: SearchString; }) => SearchString; export const useSearch: BrowserSearchHook; export const usePathname: (options?: { ssrPath?: Path }) => Path; export const useHistoryState: () => T; export const navigate: ( to: string | URL, options?: { replace?: boolean; state?: S; transition?: boolean } ) => void; /* * Default `useLocation` */ // The type of the default `useLocation` hook that wouter uses. // It operates on current URL using History API, supports base path and can // navigate with `pushState` or `replaceState`. export type BrowserLocationHook = (options?: { ssrPath?: Path; }) => [Path, typeof navigate]; export const useBrowserLocation: BrowserLocationHook; ================================================ FILE: packages/wouter-preact/types/use-hash-location.d.ts ================================================ import { Path } from "./location-hook.js"; export function navigate( to: Path, options?: { state?: S; replace?: boolean; transition?: boolean } ): void; export function useHashLocation(options?: { ssrPath?: Path; }): [Path, typeof navigate]; ================================================ FILE: specs/view-transitions-spec.md ================================================ # View Transitions API in Wouter View Transitions are baseline available (as of Oct 2025). This doc describes the API for using them in wouter. Though the browser API is super simple, there are certain obstacles to overcome: ## Problems - `startViewTransition` accepts a callback that must modify the DOM synchronously - `setState` can't guarantee that the DOM will be modified synchronously - There is `flushSync` but it requires `react-dom`, we want wouter to only depend on `react` - Wouter uses `useSyncExternalStore` to react to events. In theory sending event inside `flushSync` should trigger updates synchronously, but this is not 100% proven and could break ## Solution Users implement their own behavior before and after navigate is called, so they can control view transitions behavior. ### Basic Implementation (enable view transitions by default) ```js import { flushSync } from "react-dom"; function aroundNav(navigate, ...navArgs) { // Feature detection for older browsers if (!document.startViewTransition) { navigate(...navArgs); return; } document.startViewTransition(() => { flushSync(() => { navigate(...navArgs); }); }); } ; ``` Alternatively, with explicit arguments: ```js function aroundNav(navigate, to, options) { if (!document.startViewTransition) { navigate(to, options); return; } document.startViewTransition(() => { flushSync(() => { navigate(to, options); }); }); } ``` ### Granular control (opt-in transitions) For more control over when transitions occur: ```jsx // In your component Home ; // Or programmatically const [location, navigate] = useLocation(); navigate("/", { transition: true }); ``` **Note:** The `transition` prop is now part of wouter's type definitions (`NavigateOptions`) and is available on all location hooks (`useBrowserLocation`, `useHashLocation`, `memoryLocation`). When `` calls `navigate(targetPath, props)`, all props are automatically passed as navigation options, making them available in `aroundNav`. ```js import { flushSync } from "react-dom"; function aroundNav(navigate, to, options) { // Feature detection if (!document.startViewTransition) { navigate(to, options); return; } // Only use transitions when explicitly requested if (options?.transition) { document.startViewTransition(() => { flushSync(() => { navigate(to, options); }); }); } else { navigate(to, options); } } ``` ### TypeScript types Wouter provides built-in types for view transitions: ```typescript import type { NavigateOptions, AroundNavHandler } from "wouter"; // NavigateOptions already includes transition const navigate = (to: string, options?: NavigateOptions) => { // options.transition is available // options.replace is available // options.state is available }; // AroundNavHandler type for the aroundNav callback const aroundNav: AroundNavHandler = (navigate, to, options) => { if (options?.transition) { // handle transition } navigate(to, options); }; ``` The `transition` option is included in `NavigateOptions` along with `replace` and `state`, and is available on all built-in location hooks. ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "lib": ["ESNext", "DOM"], "moduleResolution": "bundler", "module": "Preserve", "moduleDetection": "force", "jsx": "react-jsx", "strict": true, "baseUrl": ".", "types": ["bun", "@testing-library/jest-dom"], "skipLibCheck": true }, "include": ["packages/wouter/**/*", "packages/wouter-preact/**/*"] }