We use cookies to enhance your experience on the site
CodeWorlds

Shallow Routing - Navigation Without Page Reload

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.

What is Shallow Routing?

Shallow Routing in Next.js allows changing the URL without:

  • Triggering data functions (getData, generateMetadata, etc.)
  • Re-rendering components
  • Losing client component state

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.

How to Use Shallow Routing in Next.js 15?

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.

Reading Parameters in Components

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}

Shallow Routing Use Cases

1. Filtering and Sorting Systems

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}

2. Pagination

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}

3. Sortable Tables

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}

Advantages of Shallow Routing

  1. State preservation - Component state is preserved, ensuring a smooth user experience.
  2. Better performance - No full page reload means faster interface responses.
  3. Link sharing capability - The URL is updated, so users can share a link to a specific view with specific filters.
  4. Browser history - Each change is saved in browser history, so the "back" button works as expected.

Limitations and Notes

  1. SEO compatibility - If filters and sorting are important for SEO, it may be necessary to use static site generation (SSG) for key parameter combinations.

  2. 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.

  3. Code execution order - Remember that during shallow routing, data functions are not executed, so logic based on these functions will not be triggered.

  4. 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]);

Comparison with Pages Router Methods

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 });

Full Implementation Example: Quantum Properties Search Panel

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}

Summary

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.

Go to CodeWorlds