Next.js App Router integration for @astroapps/client providing useNextNavigationService hook and automatic URL query syncing. Use when building Next.js 14+ applications with navigation and query parameter management.
@astroapps/client-nextjs provides Next.js App Router integration for @astroapps/client. It bridges Next.js navigation hooks with the NavigationService interface, enabling seamless routing and query parameter management in Next.js 14+ applications.
When to use: Use this library when building a Next.js App Router application that needs integration with @astroapps/client for navigation, query parameter syncing, and form state management.
Package: @astroapps/client-nextjs
Dependencies: @astroapps/client, @react-typed-forms/core, Next.js 14+, React 18+
Published to: npm
The primary hook that creates a NavigationService implementation using Next.js App Router hooks (useRouter, usePathname, ).
useSearchParamsThis library is designed for Next.js App Router (Next.js 13+). It does NOT work with the Pages Router.
Handles bidirectional synchronization between URL query parameters and form controls automatically, including debouncing and batching.
Provides the Next.js Link component as part of the NavigationService interface for client-side navigation.
"use client";
import { useNextNavigationService } from "@astroapps/client-nextjs";
import { AppContextProvider } from "@astroapps/client";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const navigation = useNextNavigationService();
const security = useSomeSecurityService(); // Your security implementation
return (
<html lang="en">
<AppContextProvider value={{ navigation, security }}>
<body>{children}</body>
</AppContextProvider>
</html>
);
}
"use client";
import { useNavigationService } from "@astroapps/client";
export default function MyPage() {
const nav = useNavigationService();
return (
<div>
{/* Current route information */}
<p>Current path: {nav.pathname}</p>
<p>Query params: {JSON.stringify(nav.query)}</p>
{/* Navigate programmatically */}
<button onClick={() => nav.push("/dashboard")}>
Go to Dashboard
</button>
{/* Use Next.js Link component */}
<nav.Link href="/about">About</nav.Link>
</div>
);
}
"use client";
import { useNavigationService, useSyncParam, StringParam } from "@astroapps/client";
import { useControl } from "@react-typed-forms/core";
export default function SearchPage() {
const nav = useNavigationService();
// Sync search input with URL query parameter "q"
const searchControl = useSyncParam(
nav.queryControl,
"q",
StringParam
);
return (
<div>
<input
value={searchControl.value || ""}
onChange={(e) => {
searchControl.value = e.target.value;
// URL automatically updates to ?q=... (debounced)
}}
placeholder="Search..."
/>
<p>Search: {searchControl.value}</p>
</div>
);
}
"use client";
import { useNextNavigationService } from "@astroapps/client-nextjs";
import { RouteData } from "@astroapps/client";
// Define route metadata
interface MyRouteData {
title: string;
requiresAuth: boolean;
}
const routes: Record<string, RouteData<MyRouteData>> = {
dashboard: {
data: { title: "Dashboard", requiresAuth: true },
},
settings: {
data: { title: "Settings", requiresAuth: true },
},
about: {
data: { title: "About", requiresAuth: false },
},
};
const defaultRoute: RouteData<MyRouteData> = {
data: { title: "Home", requiresAuth: false },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
const navigation = useNextNavigationService<MyRouteData>(routes, defaultRoute);
// Access route data
const currentRoute = navigation.route;
const pageTitle = currentRoute.data?.title || "Unknown";
const requiresAuth = currentRoute.data?.requiresAuth || false;
return (
<html lang="en">
<head>
<title>{pageTitle}</title>
</head>
<AppContextProvider value={{ navigation }}>
<body>
{requiresAuth ? <AuthGuard>{children}</AuthGuard> : children}
</body>
</AppContextProvider>
</html>
);
}
"use client";
import { useNavigationService } from "@astroapps/client";
export default function NavigationExample() {
const nav = useNavigationService();
const goToDashboard = () => {
nav.push("/dashboard");
};
const goToProfile = (userId: string) => {
nav.push(`/profile/${userId}`);
};
const updateQueryParams = () => {
// Replace current URL with new query params
nav.replace("?filter=active&sort=date");
};
const navigateWithScroll = () => {
// Navigate and scroll to top
nav.push("/long-page", { scroll: true });
};
const navigateWithoutScroll = () => {
// Navigate without scrolling
nav.push("/settings", { scroll: false });
};
return (
<div>
<button onClick={goToDashboard}>Dashboard</button>
<button onClick={() => goToProfile("123")}>Profile</button>
<button onClick={updateQueryParams}>Filter Active</button>
<button onClick={navigateWithScroll}>Long Page (scroll)</button>
<button onClick={navigateWithoutScroll}>Settings (no scroll)</button>
</div>
);
}
"use client";
import { useNavigationService, useSyncParam, convertStringParam } from "@astroapps/client";
interface SearchState {
query: string;
page: number;
category: string;
showArchived: boolean;
}
export default function AdvancedSearch() {
const nav = useNavigationService();
const query = useSyncParam(nav.queryControl, "q", StringParam);
const page = useSyncParam(
nav.queryControl,
"page",
convertStringParam(
(num) => num.toString(),
(str) => parseInt(str) || 1,
1
)
);
const category = useSyncParam(nav.queryControl, "category", StringParam);
const showArchived = useSyncParam(
nav.queryControl,
"archived",
convertStringParam(
(bool) => (bool ? "true" : "false"),
(str) => str === "true",
false
)
);
return (
<div>
<input
value={query.value || ""}
onChange={(e) => {
query.value = e.target.value;
page.value = 1; // Reset to page 1 on new search
}}
placeholder="Search..."
/>
<select
value={category.value || ""}
onChange={(e) => {
category.value = e.target.value;
page.value = 1;
}}
>
<option value="">All Categories</option>
<option value="books">Books</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<label>
<input
type="checkbox"
checked={showArchived.value}
onChange={(e) => {
showArchived.value = e.target.checked;
}}
/>
Show Archived
</label>
<div>
<button
disabled={page.value === 1}
onClick={() => page.value--}
>
Previous
</button>
<span>Page {page.value}</span>
<button onClick={() => page.value++}>
Next
</button>
</div>
{/* URL will be: ?q=search&page=2&category=books&archived=true */}
</div>
);
}
// ✅ DO - Mark components using navigation as client components
"use client";
import { useNavigationService } from "@astroapps/client";
export default function MyComponent() {
const nav = useNavigationService();
// ...
}
// ❌ DON'T - Try to use in server components
// Server components cannot use hooks
// ✅ DO - Create navigation service once in root layout
"use client";
export default function RootLayout({ children }) {
const navigation = useNextNavigationService();
return (
<AppContextProvider value={{ navigation }}>
{children}
</AppContextProvider>
);
}
// ❌ DON'T - Create multiple instances in different components
// This can cause sync issues
// ✅ DO - Use nav.Link for client-side navigation
const nav = useNavigationService();
<nav.Link href="/dashboard">Dashboard</nav.Link>
// ⚠️ CAUTION - Regular <a> tags cause full page reload
<a href="/dashboard">Dashboard</a> // Full page reload
// ✅ DO - Use replace for query param changes
nav.replace("?filter=active"); // Doesn't add to history
// ⚠️ CAUTION - push() for query changes clutters history
nav.push("?filter=active"); // Adds to browser history
Issue: "useRouter only works in Client Components"
"use client"; at the top of the fileIssue: Query parameters not syncing to URL
Issue: URL changes but component doesn't re-render
Issue: Infinite re-render loop with query params
Issue: searchParams is null
Issue: Navigation not working during SSR
navigation.queryControl.value.isReady before using navigation state during SSRIssue: Query parameters reset on page reload
Issue: Link component type errors
nav.Link which is properly typed for the NavigationService interface@astroapps/client-nextjsastrolabe-client-nextjs/