Skip to content

Commit

Permalink
next/compat/router (#42502)
Browse files Browse the repository at this point in the history
After speaking with @timneutkens, this PR provides a smoother experience to users trying to migrate over to app without affecting users in pages.

 This PR adds a new export available from `next/compat/router` that exposes a `useRouter()` hook that can be used in both `app/` and `pages/`. It differs from `next/router` in that it does not throw an error when the pages router is not mounted, and instead has a return type of `NextRouter | null`. This allows developers to convert components to support running in both `app/` and `pages/` as they are transitioning over to `app/`.

A component that before looked like this:

```tsx
import { useRouter } from 'next/router';

const MyComponent = () => {
  const { isReady, query } = useRouter();
  // ...
};
```

Will error when converted over to `next/compat/router`, as `null` cannot be destructured. Instead, developers will be able to take advantage of new hooks:

```tsx
import { useEffect } from 'react';
import { useRouter } from 'next/compat/router';
import { useSearchParams } from 'next/navigation';

const MyComponent = () => {
  const router = useRouter() // may be null or a NextRouter instance
  const searchParams = useSearchParams()

  useEffect(() => {
    if (router && !router.isReady) {
      return
    }

    // In `app/`, searchParams will be ready immediately with the values, in
    // `pages/` it will be available after the router is ready.
    const search = searchParams.get('search')
    // ...
  }, [router, searchParams])

  // ...
}
```

This component will now work in both `pages/` and `app/`. When the component is no longer used in `pages/`, you can remove the references to the compat router:

```tsx
import { useSearchParams } from 'next/navigation';

const MyComponent = () => {
  const searchParams = useSearchParams()

  // As this component is only used in `app/`, the compat router can be removed.
  const search = searchParams.get('search')

  // ...
}

```

Note that as of Next.js 13, calling `useRouter` from `next/router` will throw an error when not mounted. This now includes an error page that can be used to assist developers.

We hope to introduce a codemod that can convert instances of your `useRouter` from `next/router` to `next/compat/router` in the future.

Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
  • Loading branch information
wyattjoh and ijjk committed Nov 7, 2022
1 parent 01a1a7f commit fa19c44
Show file tree
Hide file tree
Showing 9 changed files with 44 additions and 7 deletions.
4 changes: 4 additions & 0 deletions errors/manifest.json
Expand Up @@ -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"
}
]
}
Expand Down
13 changes: 13 additions & 0 deletions 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)
17 changes: 17 additions & 0 deletions 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)
}
2 changes: 1 addition & 1 deletion packages/next/client/route-announcer.tsx
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions packages/next/client/router.ts
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/next/compat/router.d.ts
@@ -0,0 +1 @@
export * from '../dist/client/compat/router'
1 change: 1 addition & 0 deletions packages/next/compat/router.js
@@ -0,0 +1 @@
module.exports = require('../dist/client/compat/router')
1 change: 1 addition & 0 deletions packages/next/tsconfig.json
Expand Up @@ -14,6 +14,7 @@
"./*.d.ts",
"future/*.d.ts",
"image-types/global.d.ts",
"compat/*.d.ts",
"legacy/*.d.ts",
"types/compiled.d.ts"
]
Expand Down
2 changes: 1 addition & 1 deletion test/integration/typescript/pages/hello.tsx
Expand Up @@ -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)
Expand Down

0 comments on commit fa19c44

Please sign in to comment.