Skip to content

Commit

Permalink
add useRouter from next/navigation (#201)
Browse files Browse the repository at this point in the history
  • Loading branch information
tatethurston authored Oct 1, 2024
1 parent 0956f4f commit 0516c38
Show file tree
Hide file tree
Showing 15 changed files with 393 additions and 51 deletions.
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,21 @@
</Link>;
```

- Add `RouteLiteral` type. This type represents a string that confirmed to be a validated application route and can be passed to `Link` or `useRouter`. This is a TypeScript branded type.
- Add `RouteLiteral` type. This type represents a string that has been confirmed to be a validated application route and can be passed to `Link` or `useRouter`. This is a TypeScript branded type.

```ts
import { RouteLiteral } from "nextjs-routes";
```

- Refine types for `usePathname` and `useParams` from `"next/navigation"` to use `nextjs-routes` generated types.
`route` returns a `RouteLiteral`. If you construct a route string you can cast it to a `RouteLiteral` so that `Link` and `useRouter` will accept it:

```
const myRoute = `/foos/${foo}` as RouteLiteral
```

In general, prefer using the `route` helper to generate routes.

- Refine types for `usePathname`, `useRouter` and `useParams` from `"next/navigation"` to use `nextjs-routes` generated types.

- Fix generated routes when using [parallel-routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) and [intercepting-routes](https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes).

Expand Down
37 changes: 33 additions & 4 deletions examples/app/@types/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ declare module "nextjs-routes" {
[key: string]: string | string[] | undefined;
};

export type RoutedQuery<P extends Route["pathname"]> = Extract<
export type RoutedQuery<P extends Route["pathname"] = Route["pathname"]> = Extract<
Route,
{ pathname: P }
>["query"];
Expand Down Expand Up @@ -170,7 +170,10 @@ declare module "next/router" {
// prettier-ignore
declare module "next/navigation" {
export * from "next/dist/client/components/navigation";
import type { RoutedQuery, RouteLiteral } from "nextjs-routes";
import type { Route, RouteLiteral, RoutedQuery } from "nextjs-routes";
import type { AppRouterInstance as NextAppRouterInstance, NavigateOptions, PrefetchOptions } from "next/dist/shared/lib/app-router-context.shared-runtime";

type StaticRoute = Exclude<Route, { query: any }>["pathname"];

/**
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
Expand All @@ -189,7 +192,33 @@ declare module "next/navigation" {
*
* Read more: [Next.js Docs: `usePathname`](https://nextjs.org/docs/app/api-reference/functions/use-pathname)
*/
export function usePathname(): RouteLiteral;
export const usePathname = () => RouteLiteral;

type AppRouterInstance = Omit<NextAppRouterInstance, 'push' | 'replace' | 'href'> & {
push(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void;
replace(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void;
prefetch(href: StaticRoute | RouteLiteral, options?: PrefetchOptions): void;
}

/**
*
* This hook allows you to programmatically change routes inside [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components).
*
* @example
* ```ts
* "use client"
* import { useRouter } from 'next/navigation'
*
* export default function Page() {
* const router = useRouter()
* // ...
* router.push('/dashboard') // Navigate to /dashboard
* }
* ```
*
* Read more: [Next.js Docs: `useRouter`](https://nextjs.org/docs/app/api-reference/functions/use-router)
*/
export function useRouter(): AppRouterInstance;

/**
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
Expand All @@ -208,5 +237,5 @@ declare module "next/navigation" {
*
* Read more: [Next.js Docs: `useParams`](https://nextjs.org/docs/app/api-reference/functions/use-params)
*/
export function useParams<Pathname extends Route["pathname"] = Route["pathname"]>(): RoutedQuery<Pathname>;
export const useParams = <Pathname extends Route["pathname"] = Route["pathname"]>() => RoutedQuery<Pathname>;
}
2 changes: 1 addition & 1 deletion examples/app/src/app/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function Client() {
const router = useRouter();

return (
<button type="button" onClick={() => router.push("/tate")}>
<button type="button" onClick={() => router.push("/")}>
Dashboard
</button>
);
Expand Down
2 changes: 1 addition & 1 deletion examples/cjs/types/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ declare module "nextjs-routes" {
[key: string]: string | string[] | undefined;
};

export type RoutedQuery<P extends Route["pathname"]> = Extract<
export type RoutedQuery<P extends Route["pathname"] = Route["pathname"]> = Extract<
Route,
{ pathname: P }
>["query"];
Expand Down
2 changes: 1 addition & 1 deletion examples/intl/@types/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ declare module "nextjs-routes" {
[key: string]: string | string[] | undefined;
};

export type RoutedQuery<P extends Route["pathname"]> = Extract<
export type RoutedQuery<P extends Route["pathname"] = Route["pathname"]> = Extract<
Route,
{ pathname: P }
>["query"];
Expand Down
2 changes: 1 addition & 1 deletion examples/typescript/types/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ declare module "nextjs-routes" {
[key: string]: string | string[] | undefined;
};

export type RoutedQuery<P extends Route["pathname"]> = Extract<
export type RoutedQuery<P extends Route["pathname"] = Route["pathname"]> = Extract<
Route,
{ pathname: P }
>["query"];
Expand Down
80 changes: 77 additions & 3 deletions packages/e2e/@types/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ declare module "nextjs-routes" {
export type Route =
| StaticRoute<"/">
| DynamicRoute<"/[...slug]", { "slug": string[] }>
| DynamicRoute<"/bars/[bar]", { "bar": string }>
| DynamicRoute<"/foos/[foo]", { "foo": string }>;

interface StaticRoute<Pathname> {
Expand All @@ -31,7 +32,7 @@ declare module "nextjs-routes" {
[key: string]: string | string[] | undefined;
};

export type RoutedQuery<P extends Route["pathname"]> = Extract<
export type RoutedQuery<P extends Route["pathname"] = Route["pathname"]> = Extract<
Route,
{ pathname: P }
>["query"];
Expand Down Expand Up @@ -82,14 +83,14 @@ declare module "nextjs-routes" {

// prettier-ignore
declare module "next/link" {
import type { Route } from "nextjs-routes";;
import type { Route, RouteLiteral } from "nextjs-routes";;
import type { LinkProps as NextLinkProps } from "next/dist/client/link";
import type React from "react";

type StaticRoute = Exclude<Route, { query: any }>["pathname"];

export type LinkProps = Omit<NextLinkProps, "href" | "locale"> & {
href: Route | StaticRoute | Omit<Route, "pathname">;
href: Route | StaticRoute | Omit<Route, "pathname"> | RouteLiteral;
locale?: false;
}

Expand Down Expand Up @@ -167,3 +168,76 @@ declare module "next/router" {

export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
}

// prettier-ignore
declare module "next/navigation" {
export * from "next/dist/client/components/navigation";
import type { Route, RouteLiteral, RoutedQuery } from "nextjs-routes";
import type { AppRouterInstance as NextAppRouterInstance, NavigateOptions, PrefetchOptions } from "next/dist/shared/lib/app-router-context.shared-runtime";

type StaticRoute = Exclude<Route, { query: any }>["pathname"];

/**
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
* that lets you read the current URL's pathname.
*
* @example
* ```ts
* "use client"
* import { usePathname } from 'next/navigation'
*
* export default function Page() {
* const pathname = usePathname() // returns "/dashboard" on /dashboard?foo=bar
* // ...
* }
* ```
*
* Read more: [Next.js Docs: `usePathname`](https://nextjs.org/docs/app/api-reference/functions/use-pathname)
*/
export const usePathname = () => RouteLiteral;

type AppRouterInstance = Omit<NextAppRouterInstance, 'push' | 'replace' | 'href'> & {
push(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void;
replace(href: StaticRoute | RouteLiteral, options?: NavigateOptions): void;
prefetch(href: StaticRoute | RouteLiteral, options?: PrefetchOptions): void;
}

/**
*
* This hook allows you to programmatically change routes inside [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components).
*
* @example
* ```ts
* "use client"
* import { useRouter } from 'next/navigation'
*
* export default function Page() {
* const router = useRouter()
* // ...
* router.push('/dashboard') // Navigate to /dashboard
* }
* ```
*
* Read more: [Next.js Docs: `useRouter`](https://nextjs.org/docs/app/api-reference/functions/use-router)
*/
export function useRouter(): AppRouterInstance;

/**
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
* that lets you read a route's dynamic params filled in by the current URL.
*
* @example
* ```ts
* "use client"
* import { useParams } from 'next/navigation'
*
* export default function Page() {
* // on /dashboard/[team] where pathname is /dashboard/nextjs
* const { team } = useParams() // team === "nextjs"
* }
* ```
*
* Read more: [Next.js Docs: `useParams`](https://nextjs.org/docs/app/api-reference/functions/use-params)
*/
export const useParams = <Pathname extends Route["pathname"] = Route["pathname"]>() => RoutedQuery<Pathname>;
}
1 change: 1 addition & 0 deletions packages/e2e/app/bars/[bar]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => <div>Bar</div>;
12 changes: 12 additions & 0 deletions packages/e2e/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<head />
<body>{children}</body>
</html>
);
}
3 changes: 2 additions & 1 deletion packages/e2e/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
9 changes: 7 additions & 2 deletions packages/e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
42 changes: 40 additions & 2 deletions packages/e2e/typetest.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import Link from "next/link";
import { LinkProps } from "next/link";
import { useRouter, RouterEvent, NextRouter } from "next/router";
import {
usePathname,
useRouter as useAppRouter,
useParams,
} from "next/navigation";
import {
route,
type Route,
type RoutedQuery,
type GetServerSideProps,
type GetServerSidePropsContext,
RouteLiteral,
} from "nextjs-routes";
import nextRoutes from "nextjs-routes/config";

Expand Down Expand Up @@ -100,11 +106,11 @@ const router = useRouter();

// pathname

expectType<"/" | "/foos/[foo]" | "/[...slug]">(router.pathname);
expectType<"/" | "/foos/[foo]" | "/bars/[bar]" | "/[...slug]">(router.pathname);

// route

expectType<"/" | "/foos/[foo]" | "/[...slug]">(router.route);
expectType<"/" | "/foos/[foo]" | "/bars/[bar]" | "/[...slug]">(router.route);

// query

Expand Down Expand Up @@ -316,3 +322,35 @@ getServerSideProps = (async (ctx) => {
},
};
}) satisfies GetServerSideProps<{}, "/foos/[foo]">;

// next/navigation
interface NavigateOptions {
scroll?: boolean;
}

enum PrefetchKind {
AUTO = "auto",
FULL = "full",
TEMPORARY = "temporary",
}

interface PrefetchOptions {
kind: PrefetchKind;
}
expectType<() => void>(useAppRouter().back);
expectType<() => void>(useAppRouter().forward);
expectType<() => void>(useAppRouter().refresh);
expectType<(href: RouteLiteral, options: NavigateOptions) => void>(
useAppRouter().push,
);
expectType<(href: RouteLiteral, options: NavigateOptions) => void>(
useAppRouter().replace,
);
expectType<(href: string, options: PrefetchOptions) => void>(
useAppRouter().prefetch,
);

expectType<RouteLiteral>(usePathname());

expectType<RoutedQuery>(useParams());
expectType<RoutedQuery<"/bars/[bar]">>(useParams<"/bars/[bar]">());
2 changes: 1 addition & 1 deletion packages/nextjs-routes/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nextjs-routes",
"version": "2.2.2-rc.4",
"version": "2.2.2",
"description": "Type safe routing for Next.js",
"license": "MIT",
"author": "Tate <[email protected]>",
Expand Down
Loading

0 comments on commit 0516c38

Please sign in to comment.