#await router push
1 messages · Page 1 of 1 (latest)
Hi @ashen pecan. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
In the Next.js App Router, the router.push() method from next/navigation no longer returns a Promise like it did in the Pages Router. This is a breaking change that isn't well-documented in the migration guides.
The reason for this change is related to React's startTransition feature, which allows the router to work with React's concurrent features. However, this means you can no longer directly await the navigation.
There are several workarounds you can implement:
Option 1: Use useTransition with a custom hook
You can create a custom hook that wraps the router navigation in a transition and resolves a promise when the navigation completes:
'use client'
import { useRouter } from 'next/navigation'
import { useEffect, useState, useTransition } from 'react'
export const useAsyncRouter = () => {
const router = useRouter();
const pathname = usePathname();
const [resolveRouteChanged, setResolveRouteChanged] = useState<(() => void) | null>(null);
useEffect(() => {
resolveRouteChanged?.();
}, [pathname]);
const asyncRouter = useMemo(() => {
const push = async <RouteType>(href: string) => {
router.push(href);
await new Promise<void>((resolve) => {
// wait for the route change to complete
setResolveRouteChanged(resolve);
});
};
return { ...router, push };
}, [router]);
return asyncRouter;
};
Option 2: Create a route observer
Another approach is to create a route observer that notifies when a route change is complete:
'use client'
import { useRouter } from 'next/navigation'
import { useEffect, useTransition } from 'react'
type ObserverCallback = () => void
const createRouteObserver = () => {
let observer: ObserverCallback | null = null
const setObserver = (callback: ObserverCallback) => {
observer = callback
}
const notify = () => {
if (observer) {
observer()
}
}
return { setObserver, notify }
}
const routeObserver = createRouteObserver()
export const useAsyncRoutePush = () => {
const [isPending, startTransition] = useTransition()
const router = useRouter()
const asyncPush = async (path: string) => {
return new Promise<void>((resolve) => {
startTransition(() => {
router.push(path)
})
routeObserver.setObserver(() => {
resolve()
})
})
}
useEffect(() => {
if (!isPending) {
routeObserver.notify()
}
}, [isPending])
return asyncPush
}
Usage:
```tsx
export default function MyComponent() {
const asyncPush = useAsyncRoutePush()
return <Button onClick={async () => {
await asyncPush('/')
console.log("Route changed")
}}>
Go / route
</Button>
}
This is a known issue in the Next.js community, and there's an open GitHub issue requesting better documentation and built-in support for this use case Docs: How to migrate from async router.push to new next/navigation push method.
Note that these are workarounds, and the Next.js team may provide an official solution in the future.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@ashen pecan Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@edgy notch
explain reason with React's startTransaction in detail
Let me check the knowledge base for you again. Give me one moment! :books:
I'll explain why Next.js changed router.push() to work with React's startTransition instead of returning a Promise.
Why Next.js Router Uses startTransition
In the App Router, Next.js leverages React's concurrent features, particularly startTransition, to improve the user experience during navigation. This is a significant architectural change from the Pages Router.
What is startTransition?
startTransition is a React API that marks state updates as "transitions" - non-urgent updates that can be interrupted by more urgent updates. This helps maintain UI responsiveness during complex rendering operations.
As explained in this GitHub issue, the integration between Next.js router and React's concurrent features is designed to make navigation feel smoother by:
- Keeping the current page interactive while the next page is being prepared
- Allowing React to prioritize more urgent user interactions
- Preventing UI freezes during navigation
The Technical Reason for the Change
The change from Promise-based navigation to startTransition-based navigation was necessary because:
-
Concurrent Rendering Compatibility: Promises don't work well with React's concurrent rendering model, which needs to be able to interrupt and resume work.
-
Preventing Race Conditions: As seen in this issue, the previous Promise-based approach could lead to race conditions when users navigate quickly between pages.
-
Suspense Integration: The App Router heavily uses React Suspense for loading states, which works better with transitions than with Promises.
In another issue, a user explains that using startTransition properly synchronizes both client-side and server-side component re-rendering:
startTransition(() => {
// calling order does not matter
reset();
router.refresh();
});
The Tradeoff
The downside, as you've experienced, is that you can no longer directly await navigation completion. This is a deliberate architectural decision that prioritizes UI responsiveness over the simplicity of Promise-based navigation.
This change has been controversial, with many developers requesting better documentation or alternative APIs, as evidenced by the open GitHub issue discussing migration paths from the old Promise-based approach.