From b98fb0825eb3208bc4cf1d79099ff2f6144319fa Mon Sep 17 00:00:00 2001 From: David Velasco Date: Sun, 2 Jun 2024 14:21:54 +0200 Subject: [PATCH 1/2] feat: useRouter transitions --- example/app/page.js | 25 ++++++++++++++++++------- src/index.ts | 1 + src/link.tsx | 17 +++-------------- src/use-router.ts | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 src/use-router.ts diff --git a/example/app/page.js b/example/app/page.js index d9aee05..e450444 100644 --- a/example/app/page.js +++ b/example/app/page.js @@ -1,17 +1,28 @@ -import { Link } from 'next-view-transitions' +'use client' + +import { Link, useRouter } from 'next-view-transitions' export default function Page() { + const router = useRouter(); + + const routerNavigate = () => { + router.push('/demo'); + } + return (

Demo

-

- Go to /demo → -

-

Disclaimer

-

- This library is aimed at basic use cases of View Transitions and Next.js +

+ Go to /demo → +

+

+ Go to /demo with router.push → +

+

Disclaimer

+

+ This library is aimed at basic use cases of View Transitions and Next.js App Router. With more complex applications and use cases like concurrent rendering, Suspense and streaming, new primitives and APIs still need to be developed into the core of React and Next.js in the future ( diff --git a/src/index.ts b/src/index.ts index 8a097d8..79963eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export { Link } from './link' export { ViewTransitions } from './transition-context' +export { useRouter } from './use-router' diff --git a/src/link.tsx b/src/link.tsx index 992f3c4..1f4a39e 100644 --- a/src/link.tsx +++ b/src/link.tsx @@ -1,5 +1,5 @@ import NextLink from 'next/link' -import { useRouter } from 'next/navigation' +import { useRouter } from './use-router' import { startTransition, useCallback } from 'react' import { useSetFinishViewTransition } from './transition-context' @@ -55,19 +55,8 @@ export function Link(props: React.ComponentProps) { e.preventDefault() - // @ts-ignore - document.startViewTransition( - () => - new Promise((resolve) => { - startTransition(() => { - // copied from https://github.com/vercel/next.js/blob/66f8ffaa7a834f6591a12517618dce1fd69784f6/packages/next/src/client/link.tsx#L231-L233 - router[replace ? 'replace' : 'push'](as || href, { - scroll: scroll ?? true, - }) - finishViewTransition(() => resolve) - }) - }) - ) + const navigate = replace ? router.replace : router.push + navigate(as || href, { scroll: scroll ?? true }) } }, [props.onClick, href, as, replace, scroll] diff --git a/src/use-router.ts b/src/use-router.ts new file mode 100644 index 0000000..65fe78b --- /dev/null +++ b/src/use-router.ts @@ -0,0 +1,35 @@ +import { useRouter as useNextRouter } from 'next/navigation' +import { startTransition, useCallback } from "react"; +import { useSetFinishViewTransition } from "./transition-context"; + +export function useRouter() { + const router = useNextRouter() + const finishViewTransition = useSetFinishViewTransition() + + const callback = useCallback((cb: () => void) => { + // @ts-ignore + document.startViewTransition( + () => + new Promise((resolve) => { + startTransition(() => { + cb(); + finishViewTransition(() => resolve) + }) + }) + ) + }, []) + + const push = useCallback((...args: Parameters) => { + callback(() => router.push(...args)) + }, [callback, router]) + + const replace = useCallback((...args: Parameters) => { + callback(() => router.replace(...args)) + }, [callback, router]); + + return { + ...router, + push, + replace, + } +} \ No newline at end of file From 4678b2853f424634a608568fd259eba56e25b334 Mon Sep 17 00:00:00 2001 From: David Velasco Date: Wed, 5 Jun 2024 00:27:40 +0200 Subject: [PATCH 2/2] feat: JS transitions support feat: memoize transition router fix: remove unused imports --- example/app/page.js | 57 +++++++++++++++++++++++++++++-- src/index.ts | 2 +- src/link.tsx | 8 ++--- src/use-router.ts | 35 ------------------- src/use-transition-router.ts | 66 ++++++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 44 deletions(-) delete mode 100644 src/use-router.ts create mode 100644 src/use-transition-router.ts diff --git a/example/app/page.js b/example/app/page.js index e450444..46a9534 100644 --- a/example/app/page.js +++ b/example/app/page.js @@ -1,12 +1,16 @@ 'use client' -import { Link, useRouter } from 'next-view-transitions' +import { Link, useTransitionRouter } from 'next-view-transitions' +import { useState } from "react"; export default function Page() { - const router = useRouter(); + const [withCustomTransition, setWithCustomTransition] = useState(false) + const router = useTransitionRouter(); const routerNavigate = () => { - router.push('/demo'); + router.push('/demo', { + onTransitionReady: withCustomTransition ? slideInOut: undefined, + }); } return ( @@ -20,6 +24,12 @@ export default function Page() {

Go to /demo with router.push →

+

+ +

Disclaimer

This library is aimed at basic use cases of View Transitions and Next.js @@ -87,3 +97,44 @@ export default function Component() {

) } + +function slideInOut() { + document.documentElement.animate( + [ + { + opacity: 1, + transform: 'translate(0, 0)', + }, + { + opacity: 0, + transform: 'translate(-100%, 0)', + }, + ], + { + duration: 500, + easing: 'ease-in-out', + fill: 'forwards', + pseudoElement: '::view-transition-old(root)', + } + ); + + document.documentElement.animate( + [ + { + opacity: 0, + transform: 'translate(100%, 0)', + }, + { + opacity: 1, + transform: 'translate(0, 0)', + }, + ], + { + duration: 500, + easing: 'ease-in-out', + fill: 'forwards', + pseudoElement: '::view-transition-new(root)', + } + ); +} + diff --git a/src/index.ts b/src/index.ts index 79963eb..f5555e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,4 @@ export { Link } from './link' export { ViewTransitions } from './transition-context' -export { useRouter } from './use-router' +export { useTransitionRouter } from './use-transition-router' diff --git a/src/link.tsx b/src/link.tsx index 1f4a39e..c70adea 100644 --- a/src/link.tsx +++ b/src/link.tsx @@ -1,7 +1,6 @@ import NextLink from 'next/link' -import { useRouter } from './use-router' -import { startTransition, useCallback } from 'react' -import { useSetFinishViewTransition } from './transition-context' +import { useTransitionRouter } from './use-transition-router' +import { useCallback } from 'react' // copied from https://github.com/vercel/next.js/blob/66f8ffaa7a834f6591a12517618dce1fd69784f6/packages/next/src/client/link.tsx#L180-L191 function isModifiedEvent(event: React.MouseEvent): boolean { @@ -38,8 +37,7 @@ function shouldPreserveDefault( // to navigate, and trigger a view transition. export function Link(props: React.ComponentProps) { - const router = useRouter() - const finishViewTransition = useSetFinishViewTransition() + const router = useTransitionRouter() const { href, as, replace, scroll } = props const onClick = useCallback( diff --git a/src/use-router.ts b/src/use-router.ts deleted file mode 100644 index 65fe78b..0000000 --- a/src/use-router.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useRouter as useNextRouter } from 'next/navigation' -import { startTransition, useCallback } from "react"; -import { useSetFinishViewTransition } from "./transition-context"; - -export function useRouter() { - const router = useNextRouter() - const finishViewTransition = useSetFinishViewTransition() - - const callback = useCallback((cb: () => void) => { - // @ts-ignore - document.startViewTransition( - () => - new Promise((resolve) => { - startTransition(() => { - cb(); - finishViewTransition(() => resolve) - }) - }) - ) - }, []) - - const push = useCallback((...args: Parameters) => { - callback(() => router.push(...args)) - }, [callback, router]) - - const replace = useCallback((...args: Parameters) => { - callback(() => router.replace(...args)) - }, [callback, router]); - - return { - ...router, - push, - replace, - } -} \ No newline at end of file diff --git a/src/use-transition-router.ts b/src/use-transition-router.ts new file mode 100644 index 0000000..6b125d3 --- /dev/null +++ b/src/use-transition-router.ts @@ -0,0 +1,66 @@ +import { useRouter as useNextRouter } from 'next/navigation' +import {startTransition, useCallback, useMemo} from "react"; +import { useSetFinishViewTransition } from "./transition-context"; +import { + AppRouterInstance, + NavigateOptions +} from "next/dist/shared/lib/app-router-context.shared-runtime"; + +export type TransitionOptions = { + onTransitionReady?: () => void; +}; + +type NavigateOptionsWithTransition = NavigateOptions & TransitionOptions; + +export type TransitionRouter = AppRouterInstance & { + push: (href: string, options?: NavigateOptionsWithTransition) => void; + replace: (href: string, options?: NavigateOptionsWithTransition) => void; +}; + +export function useTransitionRouter() { + const router = useNextRouter() + const finishViewTransition = useSetFinishViewTransition() + + const triggerTransition = useCallback((cb: () => void, { onTransitionReady }: TransitionOptions = {}) => { + // @ts-ignore + const transition = document.startViewTransition( + () => + new Promise((resolve) => { + startTransition(() => { + cb(); + finishViewTransition(() => resolve) + }) + }) + ) + + if (onTransitionReady) { + transition.ready.then(onTransitionReady); + } + }, []) + + const push = useCallback(( + href: string, + { onTransitionReady, ...options }: NavigateOptionsWithTransition = {} + ) => { + triggerTransition(() => router.push(href, options), { + onTransitionReady + }) + }, [triggerTransition, router]) + + const replace = useCallback(( + href: string, + { onTransitionReady, ...options }: NavigateOptionsWithTransition = {} + ) => { + triggerTransition(() => router.replace(href, options), { + onTransitionReady + }); + }, [triggerTransition, router]); + + return useMemo( + () => ({ + ...router, + push, + replace, + }), + [push, replace, router]); +} \ No newline at end of file