From b407ccab5d1fc69afcc048b0321ef7b02f227ad7 Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Thu, 11 Dec 2025 12:04:29 +0100 Subject: [PATCH 1/7] Implement support for `aroundNav` handler. --- packages/wouter/src/index.js | 7 +- packages/wouter/test/router.test.tsx | 43 +++++++ .../wouter/test/view-transitions.test.tsx | 106 ++++++++++++++++ packages/wouter/types/memory-location.d.ts | 2 +- packages/wouter/types/router.d.ts | 17 +++ .../wouter/types/use-browser-location.d.ts | 2 +- packages/wouter/types/use-hash-location.d.ts | 2 +- specs/view-transitions-spec.md | 118 ++++++++++++++++++ 8 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 packages/wouter/test/view-transitions.test.tsx create mode 100644 specs/view-transitions-spec.md diff --git a/packages/wouter/src/index.js b/packages/wouter/src/index.js index 07e6288e..957a7be6 100644 --- a/packages/wouter/src/index.js +++ b/packages/wouter/src/index.js @@ -40,6 +40,8 @@ const defaultRouter = { 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); @@ -71,7 +73,9 @@ const useLocationFromRouter = (router) => { // (This is achieved via `useEvent`.) return [ relativePath(router.base, location), - useEvent((to, navOpts) => navigate(absolutePath(to, router.base), navOpts)), + useEvent((to, opts) => + router.aroundNav(navigate, absolutePath(to, router.base), opts) + ), ]; }; @@ -270,6 +274,7 @@ export const Link = forwardRef((props, ref) => { /* eslint-disable no-unused-vars */ replace /* ignore nav props */, state /* ignore nav props */, + transition /* ignore nav props */, /* eslint-enable no-unused-vars */ ...restProps diff --git a/packages/wouter/test/router.test.tsx b/packages/wouter/test/router.test.tsx index b1abe151..6314c626 100644 --- a/packages/wouter/test/router.test.tsx +++ b/packages/wouter/test/router.test.tsx @@ -211,6 +211,49 @@ describe("`hrefs` prop", () => { }); }); +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, diff --git a/packages/wouter/test/view-transitions.test.tsx b/packages/wouter/test/view-transitions.test.tsx new file mode 100644 index 00000000..6c44c8bd --- /dev/null +++ b/packages/wouter/test/view-transitions.test.tsx @@ -0,0 +1,106 @@ +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("/"); + }); +}); diff --git a/packages/wouter/types/memory-location.d.ts b/packages/wouter/types/memory-location.d.ts index d40d44a0..04888b41 100644 --- a/packages/wouter/types/memory-location.d.ts +++ b/packages/wouter/types/memory-location.d.ts @@ -7,7 +7,7 @@ import { type Navigate = ( to: Path, - options?: { replace?: boolean; state?: S } + options?: { replace?: boolean; state?: S; transition?: boolean } ) => void; type HookReturnValue = { diff --git a/packages/wouter/types/router.d.ts b/packages/wouter/types/router.d.ts index 29ca142d..1110992f 100644 --- a/packages/wouter/types/router.d.ts +++ b/packages/wouter/types/router.d.ts @@ -11,6 +11,21 @@ export type Parser = ( 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; @@ -21,6 +36,7 @@ export interface RouterObject { readonly ssrPath?: Path; readonly ssrSearch?: SearchString; readonly hrefs: HrefsFormatter; + readonly aroundNav: AroundNavHandler; } // state captured during SSR render @@ -39,4 +55,5 @@ export type RouterOptions = { ssrSearch?: SearchString; ssrContext?: SsrContext; hrefs?: HrefsFormatter; + aroundNav?: AroundNavHandler; }; diff --git a/packages/wouter/types/use-browser-location.d.ts b/packages/wouter/types/use-browser-location.d.ts index 6485d76e..5b23c2e2 100644 --- a/packages/wouter/types/use-browser-location.d.ts +++ b/packages/wouter/types/use-browser-location.d.ts @@ -18,7 +18,7 @@ export const useHistoryState: () => T; export const navigate: ( to: string | URL, - options?: { replace?: boolean; state?: S } + options?: { replace?: boolean; state?: S; transition?: boolean } ) => void; /* diff --git a/packages/wouter/types/use-hash-location.d.ts b/packages/wouter/types/use-hash-location.d.ts index 3e649f5c..0fb32d3f 100644 --- a/packages/wouter/types/use-hash-location.d.ts +++ b/packages/wouter/types/use-hash-location.d.ts @@ -2,7 +2,7 @@ import { Path } from "./location-hook.js"; export function navigate( to: Path, - options?: { state?: S; replace?: boolean } + options?: { state?: S; replace?: boolean; transition?: boolean } ): void; export function useHashLocation(options?: { diff --git a/specs/view-transitions-spec.md b/specs/view-transitions-spec.md new file mode 100644 index 00000000..146e67bd --- /dev/null +++ b/specs/view-transitions-spec.md @@ -0,0 +1,118 @@ +# 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 doesn't need to be explicitly handled in wouter's source code. When `` calls `navigate(targetPath, props)` (see `packages/wouter/src/index.js:301`), all props are automatically passed as navigation options. This means any prop you add to `` becomes available in `aroundNav` options—`transition` is just a convention. + +```js +import { flushSync } from "react-dom"; + +function aroundNav(navigate, to, options) { + // Feature detection + if (!document.startViewTransition) { + navigate(to, options); + return; + } + + // TODO: Skip transitions for back/forward navigation (popstate events) + // This prevents jarring transitions when users use browser back/forward buttons + + if (options.transition) { + document.startViewTransition(() => { + flushSync(() => { + navigate(to, options); + }); + }); + } else { + navigate(to, options); + } +} +``` + +### TypeScript types + +```typescript +import type { NavigateOptions } from "wouter"; + +// Extend NavigateOptions to include transition flag +interface TransitionNavigateOptions extends NavigateOptions { + transition?: boolean; +} + +type AroundNavFunction = ( + navigate: (to: string, options?: NavigateOptions) => void, + to: string, + options: TransitionNavigateOptions +) => void; +``` From bb1b1302ae20e4efbd75fbd79577341666dd86a5 Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Thu, 11 Dec 2025 12:14:20 +0100 Subject: [PATCH 2/7] Support view transitions in the magazin example. --- packages/magazin/client.tsx | 25 +++++++++++++++++++-- packages/magazin/components/navbar.tsx | 3 +++ packages/magazin/routes/home.tsx | 1 + packages/magazin/routes/products/[slug].tsx | 2 ++ packages/magazin/styles.css | 12 ++++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/magazin/client.tsx b/packages/magazin/client.tsx index 7984e137..8182c814 100644 --- a/packages/magazin/client.tsx +++ b/packages/magazin/client.tsx @@ -1,12 +1,33 @@ import { hydrateRoot } from "react-dom/client"; -import { Router } from "wouter"; +import { flushSync } from "react-dom"; +import { Router, type NavigateOptions, 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, - + diff --git a/packages/magazin/components/navbar.tsx b/packages/magazin/components/navbar.tsx index 3a045598..ff03835d 100644 --- a/packages/magazin/components/navbar.tsx +++ b/packages/magazin/components/navbar.tsx @@ -14,6 +14,7 @@ function NavLink({ return ( `text-sm font-medium ${ active ? "text-gray-900" : "text-gray-500 hover:text-gray-900" @@ -31,6 +32,7 @@ export function Navbar() {
@@ -43,6 +45,7 @@ export function Navbar() { diff --git a/packages/magazin/routes/home.tsx b/packages/magazin/routes/home.tsx index 9a9f286a..dce45a4b 100644 --- a/packages/magazin/routes/home.tsx +++ b/packages/magazin/routes/home.tsx @@ -6,6 +6,7 @@ function ProductCard({ slug, brand, category, name, price, image }: Product) { return (
@@ -55,6 +56,7 @@ export function ProductPage({ slug }: { slug: string }) {
diff --git a/packages/magazin/styles.css b/packages/magazin/styles.css index f1d8c73c..2b6176b7 100644 --- a/packages/magazin/styles.css +++ b/packages/magazin/styles.css @@ -1 +1,13 @@ @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; +} From aac84d12cb916bd12041ab44015bb0bdc3b762da Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Thu, 11 Dec 2025 12:14:40 +0100 Subject: [PATCH 3/7] Update router.d.ts --- packages/wouter/types/router.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wouter/types/router.d.ts b/packages/wouter/types/router.d.ts index 1110992f..3838b46b 100644 --- a/packages/wouter/types/router.d.ts +++ b/packages/wouter/types/router.d.ts @@ -35,6 +35,7 @@ export interface RouterObject { readonly parser: Parser; readonly ssrPath?: Path; readonly ssrSearch?: SearchString; + readonly ssrContext?: SsrContext; readonly hrefs: HrefsFormatter; readonly aroundNav: AroundNavHandler; } From 71effd78ad11a3ebbc99e66f7e2175a2509be2cb Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Thu, 11 Dec 2025 12:24:06 +0100 Subject: [PATCH 4/7] Transition product image. --- packages/magazin/routes/home.tsx | 6 +++--- packages/magazin/routes/products/[slug].tsx | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/magazin/routes/home.tsx b/packages/magazin/routes/home.tsx index dce45a4b..da38abd4 100644 --- a/packages/magazin/routes/home.tsx +++ b/packages/magazin/routes/home.tsx @@ -7,15 +7,15 @@ function ProductCard({ slug, brand, category, name, price, image }: Product) {
{name}
-
+
{brand} · {category}
diff --git a/packages/magazin/routes/products/[slug].tsx b/packages/magazin/routes/products/[slug].tsx index 45a11c5e..5d08d167 100644 --- a/packages/magazin/routes/products/[slug].tsx +++ b/packages/magazin/routes/products/[slug].tsx @@ -34,7 +34,10 @@ export function ProductPage({ slug }: { slug: string }) {
-
+
{product.name} From 633251016de4ce64c28cb5d21d25afc23e937a01 Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Thu, 11 Dec 2025 12:48:22 +0100 Subject: [PATCH 5/7] Add View Transitions to docs. --- README.md | 69 ++++++++++++++++++++++++++++++++++ specs/view-transitions-spec.md | 40 +++++++++++--------- 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 1b587976..afcf59d7 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ projects that use wouter: **[Ultra](https://ultrajs.dev/)**, - [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) @@ -630,6 +631,16 @@ available options: - `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? @@ -837,6 +848,64 @@ export const MyComponent = ({ isVisible }) => { 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 diff --git a/specs/view-transitions-spec.md b/specs/view-transitions-spec.md index 146e67bd..63a938cb 100644 --- a/specs/view-transitions-spec.md +++ b/specs/view-transitions-spec.md @@ -73,7 +73,7 @@ const [location, navigate] = useLocation(); navigate("/", { transition: true }); ``` -**Note:** The `transition` prop doesn't need to be explicitly handled in wouter's source code. When `` calls `navigate(targetPath, props)` (see `packages/wouter/src/index.js:301`), all props are automatically passed as navigation options. This means any prop you add to `` becomes available in `aroundNav` options—`transition` is just a convention. +**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"; @@ -85,10 +85,8 @@ function aroundNav(navigate, to, options) { return; } - // TODO: Skip transitions for back/forward navigation (popstate events) - // This prevents jarring transitions when users use browser back/forward buttons - - if (options.transition) { + // Only use transitions when explicitly requested + if (options?.transition) { document.startViewTransition(() => { flushSync(() => { navigate(to, options); @@ -102,17 +100,25 @@ function aroundNav(navigate, to, options) { ### TypeScript types -```typescript -import type { NavigateOptions } from "wouter"; - -// Extend NavigateOptions to include transition flag -interface TransitionNavigateOptions extends NavigateOptions { - transition?: boolean; -} +Wouter provides built-in types for view transitions: -type AroundNavFunction = ( - navigate: (to: string, options?: NavigateOptions) => void, - to: string, - options: TransitionNavigateOptions -) => void; +```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. From b2949eeecc7284a4d2aac599d1816052034d3a8b Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Thu, 11 Dec 2025 13:09:40 +0100 Subject: [PATCH 6/7] Try to hack size limit action to use Bun, not npm. --- .github/workflows/size.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/size.yml b/.github/workflows/size.yml index 7a6498c2..413e99e1 100644 --- a/.github/workflows/size.yml +++ b/.github/workflows/size.yml @@ -14,6 +14,10 @@ jobs: 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 }} From b65dc1b86b46705fa5a496c320fffd7b081f9303 Mon Sep 17 00:00:00 2001 From: Alexey Taktarov Date: Thu, 11 Dec 2025 13:19:50 +0100 Subject: [PATCH 7/7] wouter-preact types are up to date. --- .../magazin/components/with-status-code.tsx | 3 +-- .../wouter-preact/types/memory-location.d.ts | 2 +- packages/wouter-preact/types/router.d.ts | 27 +++++++++++++++++++ .../types/use-browser-location.d.ts | 2 +- .../types/use-hash-location.d.ts | 2 +- packages/wouter/types/router.d.ts | 2 ++ 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/magazin/components/with-status-code.tsx b/packages/magazin/components/with-status-code.tsx index 51c5048d..31435bf4 100644 --- a/packages/magazin/components/with-status-code.tsx +++ b/packages/magazin/components/with-status-code.tsx @@ -10,9 +10,8 @@ export function WithStatusCode({ const router = useRouter(); // Set status code on SSR context if available - // Cast to any because statusCode is not yet in the official types if (router.ssrContext) { - (router.ssrContext as any).statusCode = code; + router.ssrContext.statusCode = code; } return <>{children}; diff --git a/packages/wouter-preact/types/memory-location.d.ts b/packages/wouter-preact/types/memory-location.d.ts index 55639038..6cfe7011 100644 --- a/packages/wouter-preact/types/memory-location.d.ts +++ b/packages/wouter-preact/types/memory-location.d.ts @@ -2,7 +2,7 @@ import { BaseLocationHook, Path } from "./location-hook.js"; type Navigate = ( to: Path, - options?: { replace?: boolean; state?: S } + options?: { replace?: boolean; state?: S; transition?: boolean } ) => void; type HookReturnValue = { hook: BaseLocationHook; navigate: Navigate }; diff --git a/packages/wouter-preact/types/router.d.ts b/packages/wouter-preact/types/router.d.ts index 8133e53b..56a0872e 100644 --- a/packages/wouter-preact/types/router.d.ts +++ b/packages/wouter-preact/types/router.d.ts @@ -11,6 +11,21 @@ export type Parser = ( 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; @@ -20,9 +35,19 @@ export interface RouterObject { 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; @@ -31,5 +56,7 @@ export type RouterOptions = { parser?: Parser; ssrPath?: Path; ssrSearch?: SearchString; + ssrContext?: SsrContext; hrefs?: HrefsFormatter; + aroundNav?: AroundNavHandler; }; diff --git a/packages/wouter-preact/types/use-browser-location.d.ts b/packages/wouter-preact/types/use-browser-location.d.ts index 6485d76e..5b23c2e2 100644 --- a/packages/wouter-preact/types/use-browser-location.d.ts +++ b/packages/wouter-preact/types/use-browser-location.d.ts @@ -18,7 +18,7 @@ export const useHistoryState: () => T; export const navigate: ( to: string | URL, - options?: { replace?: boolean; state?: S } + options?: { replace?: boolean; state?: S; transition?: boolean } ) => void; /* diff --git a/packages/wouter-preact/types/use-hash-location.d.ts b/packages/wouter-preact/types/use-hash-location.d.ts index 3e649f5c..0fb32d3f 100644 --- a/packages/wouter-preact/types/use-hash-location.d.ts +++ b/packages/wouter-preact/types/use-hash-location.d.ts @@ -2,7 +2,7 @@ import { Path } from "./location-hook.js"; export function navigate( to: Path, - options?: { state?: S; replace?: boolean } + options?: { state?: S; replace?: boolean; transition?: boolean } ): void; export function useHashLocation(options?: { diff --git a/packages/wouter/types/router.d.ts b/packages/wouter/types/router.d.ts index 3838b46b..56a0872e 100644 --- a/packages/wouter/types/router.d.ts +++ b/packages/wouter/types/router.d.ts @@ -44,6 +44,8 @@ export interface RouterObject { 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