npm package: @natemac/app-router
Description: An **abstract app-router** that works seamlessly across **Next.js 15 App Router** and **React Router v7**.   This library exposes a **unified navigation interface**, **router events**, and **standardized access** to path/params/query so your app code does
Latest version: 1.0.2

# 🧭 App Router Abstraction

An **abstract app-router** that works seamlessly across **Next.js 15 App Router** and **React Router v7**.  
This library exposes a **unified navigation interface**, **router events**, and **standardized access** to path/params/query so your app code doesn’t care which router it’s running on.

> Why this exists: shared UI across web (Next.js) and SPA (React Router) often ends up with if/else routing code. This package removes those branches by providing a stable interface.

---

## ✨ Features

- 🚦 **Unified API** for both Next.js and React Router
- 🎯 **Standardized router events**: `onNavigateStart`, `onNavigateEnd`
- 🔑 **History key tracking** per location
- 🔄 **App restart support** (useful for kiosk/TV/embedded contexts)
- 🔍 **Params & search params** exposed in a consistent shape
- ⚡ **Route prefetch** passthrough (no-op when unsupported)
- 🧱 Small, framework-agnostic provider that you adapt with tiny wrappers

---

## 📦 Installation

```bash
npm install @natemac/app-router
# or
yarn add @natemac/app-router
pnpm add @natemac/app-router
```

---

## 🧰 Core Concepts

This package centers around a single provider and a context:

- `RouterProvider` – wraps your app and wires your platform/router specifics.
- `RouterProviderContext` – consume to use `navigate`, `back`, `prefetch`, etc.
- **Events** – get callbacks when navigations start/end along with `historyKey`s.

The exposed **interface** (context value) is:

```ts
type RouterContextType = {
  back: () => void;
  navigate: (path: string) => void;
  params: object;
  prefetch: (route: string) => void;
  restartApp: () => void;
  searchParams: URLSearchParams;
};
```

Provider **props**:

```ts
type RouterProviderProps = {
  back: () => void;
  children: React.ReactNode;
  navigate: (path: string) => void;
  onNavigateEnd?: (e: { historyKey: string; pathname: string }) => void;
  onNavigateStart?: (e: {
    historyKey: string; // current page key
    nextHistoryKey: string; // about to navigate to this key
    pathname: string; // current pathname
  }) => void;
  params: object;
  pathname: string;
  prefetch: (route: string) => void;
  restartApp: () => void;
  searchParams: URLSearchParams;
};
```

> Internals such as `useCombinedEffect`, `useHistoryKey`, `useLocationStore` are implementation details of this package and **do not need** to be imported in your app.

---

## 🚀 Quick Start

Wrap your app with `RouterProvider` and pass the functions/values from your router.

### Next.js 15 (App Router)

> Place this in a **Client Component** (add `'use client'` at the top).

```tsx
"use client";

import {
  useRouter,
  usePathname,
  useSearchParams,
  useParams,
} from "next/navigation";
import { RouterProvider } from "@natemac/app-router";

export default function NextRouterAdapter({
  children,
}: {
  children: React.ReactNode;
}) {
  const router = useRouter();
  const pathname = usePathname() || "/";
  const searchParams = useSearchParams();
  const params = useParams();

  return (
    <RouterProvider
      back={() => router.back()}
      navigate={(path) => router.push(path)}
      prefetch={(path) => router.prefetch?.(path)}
      restartApp={() => window.location.reload()}
      params={params}
      pathname={pathname}
      searchParams={searchParams}
      onNavigateStart={({ pathname, nextHistoryKey }) => {
        console.log("[start]", pathname, "→", nextHistoryKey);
      }}
      onNavigateEnd={({ pathname, historyKey }) => {
        console.log("[end]", pathname, "→", , historyKey);
      }}
    >
      {children}
    </RouterProvider>
  );
}
```

### React Router v7

```tsx
import {
  useLocation,
  useNavigate,
  useParams,
  useSearchParams,
} from "react-router-dom";
import { RouterProvider } from "@natemac/app-router";

export function RR7RouterAdapter({ children }: { children: React.ReactNode }) {
  const navigateRR = useNavigate();
  const location = useLocation();
  const params = useParams();
  const [search] = useSearchParams();

  return (
    <RouterProvider
      back={() => navigateRR(-1)}
      navigate={(path) => navigateRR(path)}
      prefetch={() => {
        /* no-op in RR7 */
      }}
      restartApp={() => window.location.reload()}
      params={params as object}
      pathname={location.pathname}
      searchParams={new URLSearchParams(search.toString())}
      onNavigateStart={({ pathname, nextHistoryKey }) => {
        console.log("[start]", pathname, "→", nextHistoryKey);
      }}
      onNavigateEnd={({ pathname, historyKey }) => {
        console.log("[end]", pathname, "→", historyKey);
      }}
    >
      {children}
    </RouterProvider>
  );
}
```

---

## 🛠 Using the Context

Consume the context anywhere in your tree:

```tsx
import * as React from "react";
import { useAppRouter, useSearchParams } from "@natemac/app-router";

export function NavButtons() {
  const { navigate, back } = useAppRouter();
  const searchParams = useSearchParams();

  const id = searchParams.get("id") ?? "42";

  return (
    <div>
      <button onClick={() => back()}>Back</button>
      <button onClick={() => navigate(`/profile?id=${id}`)}>Profile</button>
    </div>
  );
}
```

---

## 📡 Navigation Events

Two optional callbacks let you observe navigation:

- **`onNavigateStart`**: fires before navigation occurs.
- **`onNavigateEnd`**: fires once the navigation is observed (history key/path updated).

Event payloads:

```ts
type NavigationEventProps = {
  historyKey: string;
  pathname: string;
};
// onNavigateStart gets { historyKey, nextHistoryKey, pathname }
```

> **Note:** `historyKey` is a best-effort unique identifier for the current entry. Different routers expose different primitives; this package normalizes them to a string key.

---

## 🧩 Patterns & Tips

- **Prefetching**: In Next.js, `router.prefetch` is supported; in RR7 it’s a no-op. Your app code can still call `prefetch` safely.
- **URLSearchParams**: Standardized as `URLSearchParams` across both routers. Next’s `ReadonlyURLSearchParams` is converted in the adapter.
- **SSR**: Place adapters inside client boundaries when using Next.js hooks.

---

## 🧱 Minimal Interface Surface (what you implement)

If you create your own adapter, you only need to provide:

- `back(): void`
- `navigate(path: string): void`
- `prefetch(route: string): void` (can be a no-op)
- `restartApp(): void`
- `params: object`
- `pathname: string`
- `searchParams: URLSearchParams`

The provider takes care of emitting events and tracking history keys.

---

## 📝 License

MIT © Nathan Macfarlane