diff --git a/errors/manifest.json b/errors/manifest.json index 5e48d7fa9d44..9be351338750 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -757,6 +757,10 @@ { "title": "invalid-segment-export", "path": "/errors/invalid-segment-export.md" + }, + { + "title": "next-router-not-mounted", + "path": "/errors/next-router-not-mounted.md" } ] } diff --git a/errors/next-router-not-mounted.md b/errors/next-router-not-mounted.md new file mode 100644 index 000000000000..0bdb3f7a9ce7 --- /dev/null +++ b/errors/next-router-not-mounted.md @@ -0,0 +1,13 @@ +# NextRouter was not mounted + +#### Why This Error Occurred + +A component used `useRouter` outside a Next.js application, or was rendered outside a Next.js application. This can happen when doing unit testing on components that use the `useRouter` hook as they are not configured with Next.js' contexts. + +#### Possible Ways to Fix It + +If used in a test, mock out the router by mocking the `next/router`'s `useRouter()` hook. + +### Useful Links + +- [next-router-mock](https://www.npmjs.com/package/next-router-mock) diff --git a/packages/next/client/compat/router.ts b/packages/next/client/compat/router.ts new file mode 100644 index 000000000000..58b1b9f02ed0 --- /dev/null +++ b/packages/next/client/compat/router.ts @@ -0,0 +1,17 @@ +import { useContext } from 'react' +import { RouterContext } from '../../shared/lib/router-context' +import { NextRouter } from '../router' + +/** + * useRouter from `next/compat/router` is designed to assist developers + * migrating from `pages/` to `app/`. Unlike `next/router`, this hook does not + * throw when the `NextRouter` is not mounted, and instead returns `null`. The + * more concrete return type here lets developers use this hook within + * components that could be shared between both `app/` and `pages/` and handle + * to the case where the router is not mounted. + * + * @returns The `NextRouter` instance if it's available, otherwise `null`. + */ +export function useRouter(): NextRouter | null { + return useContext(RouterContext) +} diff --git a/packages/next/client/route-announcer.tsx b/packages/next/client/route-announcer.tsx index cbd26f09ea7b..3b59fd84d96b 100644 --- a/packages/next/client/route-announcer.tsx +++ b/packages/next/client/route-announcer.tsx @@ -17,7 +17,7 @@ const nextjsRouteAnnouncerStyles: React.CSSProperties = { } export const RouteAnnouncer = () => { - const { asPath } = useRouter(true) + const { asPath } = useRouter() const [routeAnnouncement, setRouteAnnouncement] = React.useState('') // Only announce the path change, but not for the first load because screen diff --git a/packages/next/client/router.ts b/packages/next/client/router.ts index dbcd11a9b2d6..7f4acbd51a60 100644 --- a/packages/next/client/router.ts +++ b/packages/next/client/router.ts @@ -129,12 +129,12 @@ export default singletonRouter as SingletonRouter // Reexport the withRoute HOC export { default as withRouter } from './with-router' -export function useRouter(throwOnMissing: true): NextRouter -export function useRouter(): NextRouter -export function useRouter(throwOnMissing?: boolean) { +export function useRouter(): NextRouter { const router = React.useContext(RouterContext) - if (!router && throwOnMissing) { - throw new Error('invariant expected pages router to be mounted') + if (!router) { + throw new Error( + 'Error: NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted' + ) } return router diff --git a/packages/next/compat/router.d.ts b/packages/next/compat/router.d.ts new file mode 100644 index 000000000000..c45847372173 --- /dev/null +++ b/packages/next/compat/router.d.ts @@ -0,0 +1 @@ +export * from '../dist/client/compat/router' diff --git a/packages/next/compat/router.js b/packages/next/compat/router.js new file mode 100644 index 000000000000..1b46d4605327 --- /dev/null +++ b/packages/next/compat/router.js @@ -0,0 +1 @@ +module.exports = require('../dist/client/compat/router') diff --git a/packages/next/tsconfig.json b/packages/next/tsconfig.json index abbb87416c5f..0cbb6fd2199e 100644 --- a/packages/next/tsconfig.json +++ b/packages/next/tsconfig.json @@ -14,6 +14,7 @@ "./*.d.ts", "future/*.d.ts", "image-types/global.d.ts", + "compat/*.d.ts", "legacy/*.d.ts", "types/compiled.d.ts" ] diff --git a/test/integration/typescript/pages/hello.tsx b/test/integration/typescript/pages/hello.tsx index 1e5c82d05ec5..acfd061ee410 100644 --- a/test/integration/typescript/pages/hello.tsx +++ b/test/integration/typescript/pages/hello.tsx @@ -31,7 +31,7 @@ class Test2 extends Test { new Test2().show() export default function HelloPage(): JSX.Element { - const router = useRouter(true) + const router = useRouter() console.log(process.browser) console.log(router.pathname) console.log(router.isReady)