Files
Andrew ClarkandGitHub befa5fb082 Add experimental_gesturePush to App Router (#88776)
## Summary

This adds a new experimental router method `experimental_gesturePush`
that enables gesture-driven navigation. The feature is gated behind a
new `experimental.gestureTransition` config flag, which implicitly opts
the app into using the experimental React build.

It is _not_ suitable for production use yet; it's a proof-of-concept so
we can test the integration between the Next.js client router and
React's Gesture Transitions experiment. You can use it to build demos,
but it shouldn't be used in a real app.

It dispatches updates through React's useOptimistic hook instead of the
main router state queue, essentially forking the router state until the
gesture completes.

This should only be called inside `startGestureTransition`. It's a
low-level method that is not meant to be used in application code, only
in libraries that know how to properly integrate with both Next and
React.

## Test Plan

- E2E test that simulates a gesture by holding an async transition open
between button clicks
- Run: `pnpm testonly test/e2e/app-dir/gesture-transitions`
2026-01-20 00:35:10 -05:00

71 lines
2.1 KiB
TypeScript

'use client'
import { ReactNode, startTransition } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import Link from 'next/link'
const pendingResolvers = new Set<() => void>()
export default function Root({ children }: { children: ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const isOnHomePage = pathname === '/'
const startGesture = (href: string) => {
// NOTE: In a real scenario, this would be startGestureTransition,
// not startTransition.
startTransition(async () => {
// Call gesture push to show prefetched content immediately
;(router as any).experimental_gesturePush(href)
// Create a promise that won't resolve until the "end gesture" button
// is clicked. This simulates a gesture that takes time to complete.
await new Promise<void>((resolve) => {
pendingResolvers.add(resolve)
})
// After the gesture ends, perform the canonical navigation
router.push(href)
})
}
const endGesture = () => {
// Resolve all pending gestures
for (const resolve of pendingResolvers) {
resolve()
}
pendingResolvers.clear()
}
return (
<html>
<body>
<header>
<h1>Gesture Transitions Test</h1>
<p>
This test simulates a gesture transition using two buttons. "Start
Gesture" calls <code>experimental_gesturePush</code> and begins an
async transition that doesn't complete until "End Gesture" is
clicked. This allows observing the intermediate gesture state before
the canonical navigation completes.
</p>
</header>
<nav>
<Link href="/target-page">Link to target</Link>
<button
data-testid="start-gesture"
onClick={() => startGesture('/target-page')}
disabled={!isOnHomePage}
>
Start Gesture
</button>
<button data-testid="end-gesture" onClick={endGesture}>
End Gesture
</button>
</nav>
<main>{children}</main>
</body>
</html>
)
}