In QuantumCity, transport engineers developed a special technology that allows modifying a vehicle's route without the need to stop and restart it. This innovation, called "Shallow Trajectory Change", allows dynamic modification of trip parameters without interrupting the passenger's experience - no stops, no delays, everything happens smoothly while in motion.
In Next.js 15, there is a similar concept called "Shallow Routing". This is a powerful technique that allows updating the page URL without a full page reload, while preserving component state. It is particularly useful in applications that require dynamically changing query parameters without losing component state.
Shallow Routing in Next.js allows changing the URL without:
It is an ideal solution for filters, sorting, pagination, and other scenarios where we want to preserve the user context while updating the URL for browser history or sharing purposes.
In the App Router in Next.js 15, the shallow routing implementation relies on using the
useRouter hook from the 'next/navigation' package with the shallow: true parameter when calling the push method.Let's see how this looks in practice:
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function QuantumFilterPanel() {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 // Function to update URL parameters while preserving component state
11 const updateFilter = useCallback((name: string, value: string) => {
12 // Creating a new URLSearchParams object based on current parameters
13 const params = new URLSearchParams(searchParams.toString());
14
15 // Update or add a new parameter
16 params.set(name, value);
17
18 // Update URL without a full page reload
19 router.push('?' + params.toString(), {
20 shallow: true, // Key parameter for shallow routing
21 });
22 }, [searchParams, router]);
23
24 return (
25 <div className="quantum-filter-panel">
26 <h3>Quantum Filters</h3>
27 <div className="filter-controls">
28 <button onClick={() => updateFilter('category', 'transport')}>
29 Transport
30 </button>
31 <button onClick={() => updateFilter('category', 'accommodation')}>
32 Accommodation
33 </button>
34 <button onClick={() => updateFilter('sort', 'price_asc')}>
35 Price: ascending
36 </button>
37 <button onClick={() => updateFilter('sort', 'price_desc')}>
38 Price: descending
39 </button>
40 </div>
41 </div>
42 );
43}In the above example, clicking on filter buttons updates the URL by adding query parameters, but does not cause a full page reload. This way we preserve the application state, such as form data entered by the user, scroll position, or local component state.
To read query parameters in components, we use the
useSearchParams hook:1'use client';
2
3import { useSearchParams } from 'next/navigation';
4
5export default function QuantumResults() {
6 const searchParams = useSearchParams();
7
8 // Read query parameters
9 const category = searchParams.get('category') || 'all';
10 const sort = searchParams.get('sort') || 'default';
11
12 return (
13 <div className="quantum-results">
14 <h2>Results for category: {category}</h2>
15 <p>Sorting: {sort}</p>
16
17 {/* Here we render results based on parameters */}
18 </div>
19 );
20}An ideal use case for shallow routing is filtering and sorting systems, where the user can dynamically change data display criteria:
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function QuantumPropertyFilters() {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 const applyFilters = useCallback((filters) => {
11 const params = new URLSearchParams(searchParams.toString());
12
13 // Update parameters based on filters
14 Object.entries(filters).forEach(([key, value]) => {
15 if (value) {
16 params.set(key, value.toString());
17 } else {
18 params.delete(key);
19 }
20 });
21
22 // Update URL without page reload
23 router.push('?' + params.toString(), { shallow: true });
24 }, [searchParams, router]);
25
26 return (
27 <div className="quantum-filters">
28 <div className="filter-section">
29 <h3>Quantum Property Filters</h3>
30 <div className="filter-group">
31 <label>Minimum price:</label>
32 <input
33 type="range"
34 min="0"
35 max="10000"
36 step="100"
37 onChange={(e) => applyFilters({ minPrice: e.target.value })}
38 />
39 </div>
40 <div className="filter-group">
41 <label>Location:</label>
42 <select onChange={(e) => applyFilters({ location: e.target.value })}>
43 <option value="">All</option>
44 <option value="quantum-district">Quantum District</option>
45 <option value="nano-heights">Nano Heights</option>
46 <option value="ai-valley">AI Valley</option>
47 </select>
48 </div>
49 </div>
50 </div>
51 );
52}Shallow routing works excellently in pagination systems, where we want to preserve the current application state while navigating between pages:
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function QuantumPagination({ totalPages }) {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 // Get current page from URL parameters or default to 1
11 const currentPage = parseInt(searchParams.get('page') || '1', 10);
12
13 const goToPage = useCallback((page) => {
14 const params = new URLSearchParams(searchParams.toString());
15 params.set('page', page.toString());
16
17 // Update URL without page reload
18 router.push('?' + params.toString(), { shallow: true });
19 }, [searchParams, router]);
20
21 // Generate pagination buttons
22 const renderPaginationButtons = () => {
23 const buttons = [];
24 for (let i = 1; i <= totalPages; i++) {
25 buttons.push(
26 <button
27 key={i}
28 onClick={() => goToPage(i)}
29 className={currentPage === i ? 'active' : ''}
30 >
31 {i}
32 </button>
33 );
34 }
35 return buttons;
36 };
37
38 return (
39 <div className="quantum-pagination">
40 <button
41 disabled={currentPage === 1}
42 onClick={() => goToPage(currentPage - 1)}
43 >
44 Previous
45 </button>
46
47 {renderPaginationButtons()}
48
49 <button
50 disabled={currentPage === totalPages}
51 onClick={() => goToPage(currentPage + 1)}
52 >
53 Next
54 </button>
55 </div>
56 );
57}Another excellent use case is tables with sortable columns:
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4import { useCallback } from 'react';
5
6export default function QuantumDataTable({ data }) {
7 const router = useRouter();
8 const searchParams = useSearchParams();
9
10 // Get current sort column and direction
11 const sortColumn = searchParams.get('sort') || 'name';
12 const sortDir = searchParams.get('dir') || 'asc';
13
14 const toggleSort = useCallback((column) => {
15 const params = new URLSearchParams(searchParams.toString());
16
17 // If clicking on the same column, change sort direction
18 if (column === sortColumn) {
19 params.set('dir', sortDir === 'asc' ? 'desc' : 'asc');
20 } else {
21 // Otherwise, set new column and default direction
22 params.set('sort', column);
23 params.set('dir', 'asc');
24 }
25
26 // Update URL without page reload
27 router.push('?' + params.toString(), { shallow: true });
28 }, [searchParams, router, sortColumn, sortDir]);
29
30 // Function to render column headers with sort icons
31 const renderHeaderCell = (column, label) => {
32 const isSorted = sortColumn === column;
33 const icon = isSorted
34 ? (sortDir === 'asc' ? '↑' : '↓')
35 : '↕';
36
37 return (
38 <th
39 onClick={() => toggleSort(column)}
40 className={isSorted ? 'sorted' : ''}
41 >
42 {label} {icon}
43 </th>
44 );
45 };
46
47 return (
48 <table className="quantum-data-table">
49 <thead>
50 <tr>
51 {renderHeaderCell('name', 'Name')}
52 {renderHeaderCell('price', 'Price')}
53 {renderHeaderCell('rating', 'Rating')}
54 {renderHeaderCell('date', 'Date')}
55 </tr>
56 </thead>
57 <tbody>
58 {/* Here we render sorted data */}
59 {data
60 .sort((a, b) => {
61 const aValue = a[sortColumn];
62 const bValue = b[sortColumn];
63 const modifier = sortDir === 'asc' ? 1 : -1;
64
65 return aValue < bValue ? -1 * modifier : 1 * modifier;
66 })
67 .map(item => (
68 <tr key={item.id}>
69 <td>{item.name}</td>
70 <td>{item.price}</td>
71 <td>{item.rating}</td>
72 <td>{item.date}</td>
73 </tr>
74 ))
75 }
76 </tbody>
77 </table>
78 );
79}SEO compatibility - If filters and sorting are important for SEO, it may be necessary to use static site generation (SSG) for key parameter combinations.
Server vs. Client - Shallow routing only works on the client side, so the initial page render will always be based on parameters received from the server.
Code execution order - Remember that during shallow routing, data functions are not executed, so logic based on these functions will not be triggered.
Error handling - It is good practice to add error handling to avoid issues with invalid URL parameters:
1useEffect(() => {
2 // Check if parameters are valid
3 const page = parseInt(searchParams.get('page') || '1', 10);
4
5 if (isNaN(page) || page < 1 || page > totalPages) {
6 // Correct invalid parameters
7 const params = new URLSearchParams(searchParams.toString());
8 params.set('page', '1');
9 router.push('?' + params.toString(), { shallow: true });
10 }
11}, [searchParams, router, totalPages]);If you previously used the Pages Router in Next.js, it's worth noting the difference in shallow routing implementation:
1// In Pages Router (older approach)
2router.push('/search?query=quantumtech', undefined, { shallow: true });
3
4// In App Router (Next.js 13+)
5router.push('/search?query=quantumtech', { shallow: true });Below is an example of a full implementation of a search and filtering system for properties in QuantumCity using shallow routing:
1// app/quantum-properties/page.tsx
2'use client';
3
4import { useRouter, useSearchParams } from 'next/navigation';
5import { useState, useEffect, useCallback } from 'react';
6import { fetchProperties } from '@/lib/api';
7
8// Types for quantum properties
9interface QuantumProperty {
10 id: string;
11 name: string;
12 price: number;
13 location: string;
14 quantumLevel: number;
15 dimensionalRating: number;
16 image: string;
17}
18
19export default function QuantumPropertiesPage() {
20 const router = useRouter();
21 const searchParams = useSearchParams();
22
23 // Component state
24 const [properties, setProperties] = useState<QuantumProperty[]>([]);
25 const [loading, setLoading] = useState(true);
26
27 // Get parameters from URL
28 const location = searchParams.get('location') || 'all';
29 const minPrice = parseInt(searchParams.get('minPrice') || '0', 10);
30 const maxPrice = parseInt(searchParams.get('maxPrice') || '10000', 10);
31 const sort = searchParams.get('sort') || 'price_asc';
32 const page = parseInt(searchParams.get('page') || '1', 10);
33 const quantumLevel = parseInt(searchParams.get('quantumLevel') || '0', 10);
34
35 // Function to update filters using shallow routing
36 const updateFilters = useCallback((newFilters: Record<string, string | number>) => {
37 const params = new URLSearchParams(searchParams.toString());
38
39 // Update parameters
40 Object.entries(newFilters).forEach(([key, value]) => {
41 if (value !== undefined && value !== null && value !== '') {
42 params.set(key, value.toString());
43 } else {
44 params.delete(key);
45 }
46 });
47
48 // Always return to first page when changing filters
49 if (!('page' in newFilters)) {
50 params.set('page', '1');
51 }
52
53 // Update URL without page reload
54 router.push('?' + params.toString(), { shallow: true });
55 }, [searchParams, router]);
56
57 // Fetch data when parameters change
58 useEffect(() => {
59 const fetchData = async () => {
60 setLoading(true);
61 try {
62 // In a real application, parameters would be passed to the API
63 const data = await fetchProperties({
64 location,
65 minPrice,
66 maxPrice,
67 quantumLevel,
68 sort,
69 page
70 });
71 setProperties(data);
72 } catch (error) {
73 console.error('Error fetching properties:', error);
74 } finally {
75 setLoading(false);
76 }
77 };
78
79 fetchData();
80 }, [location, minPrice, maxPrice, sort, page, quantumLevel]);
81
82 // Sort data based on sort parameter
83 const sortedProperties = [...properties].sort((a, b) => {
84 if (sort === 'price_asc') return a.price - b.price;
85 if (sort === 'price_desc') return b.price - a.price;
86 if (sort === 'quantum_level') return b.quantumLevel - a.quantumLevel;
87 return 0;
88 });
89
90 return (
91 <div className="quantum-properties-page">
92 <h1>Quantum Properties</h1>
93
94 {/* Filters panel */}
95 <div className="filters-panel">
96 <h2>Filters</h2>
97
98 <div className="filter-group">
99 <label>Location:</label>
100 <select
101 value={location}
102 onChange={(e) => updateFilters({ location: e.target.value })}
103 >
104 <option value="all">All locations</option>
105 <option value="quantum-district">Quantum District</option>
106 <option value="nano-heights">Nano Heights</option>
107 <option value="ai-valley">AI Valley</option>
108 </select>
109 </div>
110
111 <div className="filter-group">
112 <label>Minimum price: {minPrice} QC</label>
113 <input
114 type="range"
115 min="0"
116 max="10000"
117 step="100"
118 value={minPrice}
119 onChange={(e) => updateFilters({ minPrice: e.target.value })}
120 />
121 </div>
122
123 <div className="filter-group">
124 <label>Maximum price: {maxPrice} QC</label>
125 <input
126 type="range"
127 min="0"
128 max="10000"
129 step="100"
130 value={maxPrice}
131 onChange={(e) => updateFilters({ maxPrice: e.target.value })}
132 />
133 </div>
134
135 <div className="filter-group">
136 <label>Quantum level (min): {quantumLevel}</label>
137 <input
138 type="range"
139 min="0"
140 max="10"
141 value={quantumLevel}
142 onChange={(e) => updateFilters({ quantumLevel: e.target.value })}
143 />
144 </div>
145
146 <div className="filter-group">
147 <label>Sorting:</label>
148 <select
149 value={sort}
150 onChange={(e) => updateFilters({ sort: e.target.value })}
151 >
152 <option value="price_asc">Price: ascending</option>
153 <option value="price_desc">Price: descending</option>
154 <option value="quantum_level">Quantum level</option>
155 </select>
156 </div>
157 </div>
158
159 {/* Search results */}
160 <div className="search-results">
161 {loading ? (
162 <div className="loading-spinner">Loading...</div>
163 ) : (
164 <>
165 <h2>Found {sortedProperties.length} properties</h2>
166
167 <div className="properties-grid">
168 {sortedProperties.map(property => (
169 <div key={property.id} className="property-card">
170 <img src={property.image} alt={property.name} />
171 <h3>{property.name}</h3>
172 <p className="price">{property.price} QC</p>
173 <p className="location">{property.location}</p>
174 <p className="quantum-level">
175 Quantum level: {property.quantumLevel}/10
176 </p>
177 </div>
178 ))}
179 </div>
180
181 {/* Pagination */}
182 <div className="pagination">
183 <button
184 disabled={page === 1}
185 onClick={() => updateFilters({ page: page - 1 })}
186 >
187 Previous
188 </button>
189
190 <span className="current-page">Page {page}</span>
191
192 <button onClick={() => updateFilters({ page: page + 1 })}>
193 Next
194 </button>
195 </div>
196 </>
197 )}
198 </div>
199 </div>
200 );
201}Shallow Routing in Next.js 15 is a powerful technique that allows creating interactive and dynamic user interfaces while preserving application state during URL updates. It is an ideal solution for filtering, sorting, pagination, and other use cases where we want to smoothly update page content without a full reload.
Just as "Shallow Trajectory Change" in QuantumCity allows vehicles to dynamically change routes without stopping, Shallow Routing in Next.js allows dynamic URL parameter updates without interrupting the user experience.
In the next chapter, we will discuss handling 404 pages and redirect techniques in Next.js 15, which are essential for creating complete and error-resilient applications.