Synchronize grid state with URL query parameters.
URL sync keeps the grid's state synchronized with URL query parameters. This enables shareable URLs, browser back/forward navigation, and bookmarkable grid states.
<ActiveGrid
gridId="users"
enableUrlSync={true}
columns={columns}
data={data}
{...}
/>Enable URL sync with a unique gridId:
<ActiveGrid
gridId="users" // Required for URL sync
enableUrlSync={true}
columns={columns}
data={data}
{...}
/>The gridId prop is required when enableUrlSync is true. It prefixes URL parameters to avoid conflicts.
The following state is synchronized with URL parameters:
Not synced:
Parameters are prefixed with the gridId:
?users_page=2&users_pageSize=50&users_sort=name&users_sortDir=asc{gridId}_page - Current page (1-based){gridId}_pageSize - Rows per page{gridId}_sort - Sort column ID{gridId}_sortDir - Sort direction (asc/desc){gridId}_filter_{columnId} - Column filter value{gridId}_search - Global search queryUsers can share URLs with specific grid states:
URL sync enables browser back/forward buttons to navigate through grid states:
Users can bookmark specific grid views:
Use different gridId values to avoid conflicts:
URL will contain both:
Grid automatically reads URL parameters on mount:
Update URL programmatically:
URL sync works seamlessly with server mode:
You can use both URL sync and persistence:
Priority: URL parameters take precedence over persisted state.
Works with Next.js 13+ App Router:
Works with Next.js Pages Router:
Works with React Router:
Complex filter values are JSON-encoded:
URL parameters don't negatively impact SEO. Search engines typically ignore query parameters for indexing.
For SEO-critical pages, consider:
Track grid interactions via URL changes:
gridId per gridgridId for different gridsBrowsers have URL length limits (~2,000 characters):
For complex state, consider:
The grid gracefully handles invalid URL parameters:
Enable deep linking to specific grid states:
Test URL sync behavior:
?users_sort=name&users_sortDir=asc?users_filter_status=active?users_page=3&users_pageSize=50?users_page=2&users_pageSize=25&users_sort=createdAt&users_sortDir=desc&users_filter_status=active&users_search=johnhttps://app.example.com/users?users_filter_status=active&users_sort=name?active-users_page=1&inactive-users_page=2function ShareButton() {
const handleShare = () => {
const url = window.location.href;
navigator.clipboard.writeText(url);
toast.success('Link copied to clipboard');
};
return <Button onClick={handleShare}>Share View</Button>;
}// User filters by "active" → URL updates
// User clicks browser back → Filter is restored
// User clicks browser forward → Filter reapplied<ActiveGrid
gridId="active-users"
enableUrlSync={true}
{...}
/>
<ActiveGrid
gridId="inactive-users"
enableUrlSync={true}
{...}
/>// URL: ?users_filter_status=active&users_sort=name
// Grid loads with status filtered to "active" and sorted by nameimport { useSearchParams } from 'next/navigation';
function UsersTable() {
const searchParams = useSearchParams();
const router = useRouter();
const applyFilter = (status: string) => {
const params = new URLSearchParams(searchParams);
params.set('users_filter_status', status);
router.push(`?${params.toString()}`);
};
return <Button onClick={() => applyFilter('active')}>Show Active</Button>;
}<ActiveGrid
gridId="users"
mode="server"
fetchFn={fetchUsers}
enableUrlSync={true}
columns={columns}
// Server receives params from URL
{...}
/><ActiveGrid
gridId="users"
enableUrlSync={true}
enablePersistence={true}
columns={columns}
data={data}
{...}
/>function ClearFilters() {
const router = useRouter();
const handleClear = () => {
// Remove all grid parameters
const params = new URLSearchParams(window.location.search);
Array.from(params.keys())
.filter(key => key.startsWith('users_'))
.forEach(key => params.delete(key));
router.push(`?${params.toString()}`);
};
return <Button onClick={handleClear}>Clear Filters</Button>;
}import { ActiveGrid } from '@workspace/active-grid';
import { useSearchParams, useRouter } from 'next/navigation';
function UsersTable() {
const searchParams = useSearchParams();
const router = useRouter();
const handleShare = () => {
const url = window.location.href;
navigator.clipboard.writeText(url);
toast.success('Shareable link copied!');
};
const hasFilters = Array.from(searchParams.keys())
.some(key => key.startsWith('users_filter_'));
const handleClearFilters = () => {
const params = new URLSearchParams(searchParams);
Array.from(params.keys())
.filter(key => key.startsWith('users_filter_'))
.forEach(key => params.delete(key));
router.push(`?${params.toString()}`);
};
return (
<div>
<div className="mb-4 flex gap-2">
<Button onClick={handleShare}>
<Share className="mr-2 h-4 w-4" />
Share View
</Button>
{hasFilters && (
<Button variant="outline" onClick={handleClearFilters}>
Clear Filters
</Button>
)}
</div>
<ActiveGrid
gridId="users"
enableUrlSync={true}
columns={columns}
data={users}
toolbar={{
search: { placeholder: 'Search users...' },
}}
pagination={{
enabled: true,
pageSizeOptions: [10, 25, 50],
}}
/>
</div>
);
}'use client';
import { useSearchParams, useRouter } from 'next/navigation';
function UsersTable() {
return (
<ActiveGrid
gridId="users"
enableUrlSync={true}
{...}
/>
);
}import { useRouter } from 'next/router';
function UsersTable() {
return (
<ActiveGrid
gridId="users"
enableUrlSync={true}
{...}
/>
);
}import { useSearchParams } from 'react-router-dom';
function UsersTable() {
return (
<ActiveGrid
gridId="users"
enableUrlSync={true}
{...}
/>
);
}// Simple value
?users_filter_status=active
// Complex value (with operator)
?users_filter_price=%7B%22operator%22%3A%22greaterThan%22%2C%22value%22%3A100%7D
// Decoded: {"operator":"greaterThan","value":100}useEffect(() => {
// Track page views with grid state
analytics.page(window.location.pathname + window.location.search);
}, [searchParams]);// Good: Sync essential state only
?users_page=2&users_sort=name&users_filter_status=active
// Bad: Too many parameters
?users_page=2&users_sort=name&users_filter_status=active&users_filter_role=admin&users_filter_department=sales&...// Invalid page → defaults to page 1
?users_page=invalid
// Invalid sort column → no sorting applied
?users_sort=nonexistent
// Invalid filter value → filter ignored
?users_filter_status=invalid_value// Link to filtered view
<Link href="/users?users_filter_status=active&users_sort=name">
View Active Users
</Link>
// Link to specific page
<Link href="/users?users_page=5&users_pageSize=50">
Page 5 (50 items)
</Link>import { render } from '@testing-library/react';
import { useSearchParams } from 'next/navigation';
jest.mock('next/navigation', () => ({
useSearchParams: jest.fn(),
useRouter: jest.fn(),
}));
describe('UsersTable', () => {
it('loads state from URL', () => {
(useSearchParams as jest.Mock).mockReturnValue(
new URLSearchParams('users_filter_status=active')
);
render(<UsersTable />);
// Assert filter is applied
});
});type User = {
id: string;
name: string;
};
<ActiveGrid<User>
gridId="users"
enableUrlSync={true}
columns={columns}
data={users}
{...}
/>