diff --git a/packages/docs/src/pages/ApiComponentsPage.tsx b/packages/docs/src/pages/ApiComponentsPage.tsx index 67dc4ba..22a4a8b 100644 --- a/packages/docs/src/pages/ApiComponentsPage.tsx +++ b/packages/docs/src/pages/ApiComponentsPage.tsx @@ -102,6 +102,21 @@ export function ApiComponentsPage() { takes over. See the SSR guide for details. + + + transitionTypes + + + GetTransitionTypes + + + Function returning the React transition types to attach to each navigation via{" "} + addTransitionType (React 19.3+). Receives the destination{" "} + url and navigationType; the returned types replace the + default ["navigation"]. See the{" "} + Transitions guide for details. + + features diff --git a/packages/docs/src/pages/LearnTransitionsPage.tsx b/packages/docs/src/pages/LearnTransitionsPage.tsx index 03ed2e0..55bde8d 100644 --- a/packages/docs/src/pages/LearnTransitionsPage.tsx +++ b/packages/docs/src/pages/LearnTransitionsPage.tsx @@ -64,7 +64,7 @@ function UserDetail({ data }: { data: Promise }) {

- Tagging Navigations with experimentalTransitionTypes + Tagging Navigations with transitionTypes

React's{" "} @@ -77,15 +77,14 @@ function UserDetail({ data }: { data: Promise }) { "navigation" by default.

- You can replace the default by passing an experimentalTransitionTypes{" "} - function to {""}. The function receives the destination{" "} - url and the underlying navigationType (one of push,{" "} - replace, reload, traverse) and returns an array of - transition types: + You can replace the default by passing a transitionTypes function to{" "} + {""}. The function receives the destination url and the + underlying navigationType (one of push, replace,{" "} + reload, traverse) and returns an array of transition types:

{` [ + transitionTypes={({ navigationType, url }) => [ "navigation", \`navigation-\${navigationType}\`, url.pathname.startsWith("/admin") ? "admin" : "public", @@ -95,14 +94,14 @@ function UserDetail({ data }: { data: Promise }) { The returned types replace the default — include{" "} "navigation" yourself if you want it.

-
- React Canary required. At the time of writing,{" "} - addTransitionType is only available in React Canary (exported as{" "} - unstable_addTransitionType). On stable React, the prop is still invoked but - the returned types are silently discarded. The prop is named with an{" "} - experimental prefix to reflect this; it will be renamed once{" "} - addTransitionType becomes stable in React. -
+

+ addTransitionType is available in React 19.3 or later. On older React + versions, the function is still invoked but the returned types are silently discarded. +

+

+ The prop was previously named experimentalTransitionTypes. The old name is + still accepted as a deprecated alias and will be removed in the next major version. +

diff --git a/packages/router/src/Router/index.tsx b/packages/router/src/Router/index.tsx index 1a63ea1..9d09c86 100644 --- a/packages/router/src/Router/index.tsx +++ b/packages/router/src/Router/index.tsx @@ -111,20 +111,22 @@ export type RouterProps = { */ trailingSlash?: TrailingSlashMode; /** - * **Experimental.** Function returning the React transition types to attach - * to entry-change transitions via `addTransitionType`. Called inside - * `startTransition` for each navigation; the returned types replace the - * default `["navigation"]`. + * Function returning the React transition types to attach to entry-change + * transitions via `addTransitionType`. Called inside `startTransition` for + * each navigation; the returned types replace the default `["navigation"]`. * - * Requires a React build that exports `addTransitionType` (currently React - * Canary). On builds that don't expose the API, the function is still + * Requires a React build that exports `addTransitionType` (React 19.3 or + * later). On builds that don't expose the API, the function is still * invoked but the types are silently discarded. * - * The `experimental` prefix will be dropped once `addTransitionType` - * becomes stable in React. - * * @default () => ["navigation"] */ + transitionTypes?: GetTransitionTypes; + /** + * @deprecated Use {@link RouterProps.transitionTypes} instead. This alias + * will be removed in the next major version. When both are given, + * `transitionTypes` takes precedence. + */ experimentalTransitionTypes?: GetTransitionTypes; /** * Opt-in feature flags that change the Router's behavior. @@ -170,6 +172,7 @@ export function Router({ fallback = "none", ssr, trailingSlash, + transitionTypes, experimentalTransitionTypes, features, }: RouterProps): ReactNode { @@ -230,10 +233,11 @@ export function Router({ // Resolve transition types for the current navigation. Wrapped in // useEffectEvent so the subscription effect doesn't re-run when the - // `experimentalTransitionTypes` prop identity changes. - const getTransitionTypes = useEffectEvent((context: TransitionTypeContext): readonly string[] => - experimentalTransitionTypes ? experimentalTransitionTypes(context) : ["navigation"], - ); + // `transitionTypes` prop identity changes. + const getTransitionTypes = useEffectEvent((context: TransitionTypeContext): readonly string[] => { + const getTypes = transitionTypes ?? experimentalTransitionTypes; + return getTypes ? getTypes(context) : ["navigation"]; + }); // Subscribe to navigation changes (conditionally wrapped in transition) useEffect(() => { diff --git a/packages/router/src/__tests__/Router.test.tsx b/packages/router/src/__tests__/Router.test.tsx index a0d59f5..8915796 100644 --- a/packages/router/src/__tests__/Router.test.tsx +++ b/packages/router/src/__tests__/Router.test.tsx @@ -295,7 +295,7 @@ describe("Router", () => { }); }); - describe("experimentalTransitionTypes", () => { + describe("transitionTypes", () => { let addTransitionTypeSpy: ReturnType; beforeEach(() => { @@ -324,7 +324,7 @@ describe("Router", () => { expect(addTransitionTypeSpy).toHaveBeenCalledWith("navigation"); }); - it("uses the returned types when experimentalTransitionTypes is provided", () => { + it("uses the returned types when transitionTypes is provided", () => { const getTypes: GetTransitionTypes = vi.fn(() => ["page", "fade"]); const routes: RouteDefinition[] = [ @@ -332,7 +332,7 @@ describe("Router", () => { { path: "/about", component: () =>
About
}, ]; - render(); + render(); addTransitionTypeSpy.mockClear(); @@ -346,6 +346,54 @@ describe("Router", () => { expect(addTransitionTypeSpy).not.toHaveBeenCalledWith("navigation"); }); + it("supports the deprecated experimentalTransitionTypes alias", () => { + const getTypes: GetTransitionTypes = () => ["legacy"]; + + const routes: RouteDefinition[] = [ + { path: "/", component: () =>
Home
}, + { path: "/about", component: () =>
About
}, + ]; + + render(); + + addTransitionTypeSpy.mockClear(); + + act(() => { + mockNavigation.__simulateNavigation("http://localhost/about"); + }); + + expect(addTransitionTypeSpy).toHaveBeenCalledTimes(1); + expect(addTransitionTypeSpy).toHaveBeenCalledWith("legacy"); + }); + + it("prefers transitionTypes over experimentalTransitionTypes", () => { + const getTypes: GetTransitionTypes = () => ["new"]; + const getLegacyTypes: GetTransitionTypes = vi.fn(() => ["legacy"]); + + const routes: RouteDefinition[] = [ + { path: "/", component: () =>
Home
}, + { path: "/about", component: () =>
About
}, + ]; + + render( + , + ); + + addTransitionTypeSpy.mockClear(); + + act(() => { + mockNavigation.__simulateNavigation("http://localhost/about"); + }); + + expect(addTransitionTypeSpy).toHaveBeenCalledTimes(1); + expect(addTransitionTypeSpy).toHaveBeenCalledWith("new"); + expect(getLegacyTypes).not.toHaveBeenCalled(); + }); + it("passes the new URL and navigationType to the callback", () => { const calls: TransitionTypeContext[] = []; const getTypes: GetTransitionTypes = (ctx) => { @@ -358,7 +406,7 @@ describe("Router", () => { { path: "/about", component: () =>
About
}, ]; - render(); + render(); // Push act(() => { diff --git a/packages/router/src/core/addTransitionType.ts b/packages/router/src/core/addTransitionType.ts index d868584..aeac5aa 100644 --- a/packages/router/src/core/addTransitionType.ts +++ b/packages/router/src/core/addTransitionType.ts @@ -7,8 +7,9 @@ const candidate = reactExports.addTransitionType ?? reactExports.unstable_addTra /** * Wrapper around React's `addTransitionType` that degrades to a no-op when - * the host React build doesn't expose the API (e.g. stable React 19.2). - * `addTransitionType` is currently available only in React Canary. + * the host React build doesn't expose the API (e.g. React 19.2). + * `addTransitionType` is available in React starting with 19.3; older Canary + * builds export it as `unstable_addTransitionType`. */ export const addTransitionType: AddTransitionType = typeof candidate === "function" ? (candidate as AddTransitionType) : () => {}; diff --git a/packages/router/src/types.ts b/packages/router/src/types.ts index 52f0039..3098401 100644 --- a/packages/router/src/types.ts +++ b/packages/router/src/types.ts @@ -109,7 +109,7 @@ export type OnNavigateInfo = { export type NavigationType = "push" | "replace" | "reload" | "traverse"; /** - * Context passed to an `experimentalTransitionTypes` callback. + * Context passed to a `transitionTypes` callback. */ export type TransitionTypeContext = { /** URL of the new navigation entry. */ @@ -122,7 +122,7 @@ export type TransitionTypeContext = { * Returns the React transition types to associate with a navigation entry * change. Called inside `startTransition` for each navigation; the return * value is passed to React's `addTransitionType`. Requires a React build - * that exports `addTransitionType` (currently React Canary). + * that exports `addTransitionType` (React 19.3 or later). */ export type GetTransitionTypes = (context: TransitionTypeContext) => readonly string[];