QuickGrid makes it easy to display data in a Blazor application without building a complete table component from scratch. It provides common features such as sorting, pagination, and column definitions while keeping the implementation relatively small.
But there is one problem developers often run into when building a real application.
A user changes the page, sorts a column, or applies some table state, and then refreshes the browser. The table goes back to its default state.
The same thing happens when a user copies the page URL and sends it to someone else. The recipient gets the page, but not necessarily the same table state.
A better approach is to keep the important grid state in the URL.
For example:
/products?page=3&sort=price&direction=desc
Now the URL describes the current view.
The user can refresh the page, bookmark it, or share it with another person without losing the selected page and sort order.
This article shows how to build that pattern with Blazor and QuickGrid, how the URL state works, and what to consider when using it in a real application.
Why Keep Grid State in the URL?
Pagination and sorting are part of the user's current view of the data.
If that state exists only inside the component, it can disappear when the component is recreated or the browser is refreshed.
Consider a product table with 500 records.
A user performs these actions:
Opens the product page.
Moves to page 4.
Sorts by price.
Finds the records they need.
Copies the URL.
Sends it to another developer.
If the URL contains only:
/products
the recipient has no way to know which page or sorting option the first user was looking at.
With URL state, the link can contain:
/products?page=4&sort=price&direction=desc
The URL becomes a small representation of the current view.
This is useful for:
Browser refreshes
Bookmarks
Sharing links
Browser history
Support and troubleshooting
Reproducing a particular data view
How URL State Works in Blazor
Blazor provides routing and navigation services that can be used to read and update the current URL.
The main service used for this is NavigationManager.
A basic component can inject it like this:
@inject NavigationManager Navigation
You can then read the current URI:
var currentUri = Navigation.Uri;
For example, if the browser is currently on:
https://example.com/products?page=2&sort=name
the component can inspect the query string and restore the table state.
The general flow looks like this:
Browser URL
|
v
Read query parameters
|
v
Create grid state
|
v
Display QuickGrid
|
v
User changes sorting/page
|
v
Update URL
The important part is keeping the URL and the component state synchronized.
Creating a Product Model
Let's start with a simple model.
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public decimal Price { get; set; }
public bool IsAvailable { get; set; }
}
For this example, assume the application has a collection of products.
private List<Product> Products = new()
{
new Product
{
Id = 1,
Name = "Laptop",
Category = "Electronics",
Price = 75000,
IsAvailable = true
},
new Product
{
Id = 2,
Name = "Keyboard",
Category = "Accessories",
Price = 2500,
IsAvailable = true
},
new Product
{
Id = 3,
Name = "Monitor",
Category = "Electronics",
Price = 18000,
IsAvailable = true
}
};
A production application would normally load this data from a database or API.
Adding QuickGrid
QuickGrid can then display the collection.
<QuickGrid Items="@Products.AsQueryable()">
<PropertyColumn Property="p => p.Id" Title="ID" />
<PropertyColumn Property="p => p.Name" Title="Name" />
<PropertyColumn Property="p => p.Category" Title="Category" />
<PropertyColumn Property="p => p.Price" Title="Price" />
<PropertyColumn Property="p => p.IsAvailable" Title="Available" />
</QuickGrid>
This gives us a basic sortable data table.
However, the sorting state is still owned by the grid.
If we want the URL to represent that state, we need to connect the grid state with navigation.
Defining the URL Parameters
Keep the URL format simple.
For example:
/products?page=2&sort=price&direction=desc
Here:
Parameter | Meaning |
|---|---|
| Current page number |
| Column being sorted |
| Sort direction |
A component can represent this state using a small class.
private int CurrentPage = 1;
private string SortColumn = "name";
private bool SortDescending;
These values become the source of truth for the current table view.
Reading the Query String
Blazor applications can read query-string values from the current URI.
One simple approach is to use QueryHelpers.
using Microsoft.AspNetCore.WebUtilities;
Then parse the query string:
private void ReadGridStateFromUrl()
{
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
var query = QueryHelpers.ParseQuery(uri.Query);
if (query.TryGetValue("page", out var pageValue) &&
int.TryParse(pageValue, out var page))
{
CurrentPage = Math.Max(page, 1);
}
if (query.TryGetValue("sort", out var sortValue))
{
SortColumn = sortValue.ToString();
}
if (query.TryGetValue("direction", out var directionValue))
{
SortDescending =
string.Equals(
directionValue.ToString(),
"desc",
StringComparison.OrdinalIgnoreCase);
}
}
The validation is important.
Do not assume that a URL always contains valid values.
A user can manually enter:
/products?page=hello
or:
/products?page=-50
Your component should handle those values safely.
Updating the URL
When the user changes the grid state, update the URL.
A helper method makes this easier:
private void UpdateGridUrl()
{
var query = new Dictionary<string, object?>
{
["page"] = CurrentPage,
["sort"] = SortColumn,
["direction"] = SortDescending ? "desc" : "asc"
};
var uri = Navigation.GetUriWithQueryParameters(query);
Navigation.NavigateTo(uri);
}
This keeps URL generation in one place.
If the user changes the page, call:
CurrentPage = 3;
UpdateGridUrl();
If the user changes sorting:
SortColumn = "price";
SortDescending = true;
UpdateGridUrl();
The browser URL can then become:
/products?page=3&sort=price&direction=desc
Avoiding Unnecessary History Entries
There is an important detail when updating URLs.
If every grid click creates a browser history entry, users may need to press the Back button many times to leave the page.
For example:
/products?page=1
/products?page=2
/products?page=3
/products?page=4
/products?page=5
If a user goes from page 1 to page 5, they probably do not expect four separate browser history entries.
For state changes that should not create a new navigation history entry, use the navigation option appropriate for replacing the current history entry.
Navigation.NavigateTo(uri, replace: true);
This gives a cleaner browser experience.
The exact behavior you want depends on the application.
For a search page where users may want Back to restore previous searches, adding history entries may make sense.
For rapidly changing grid state, replacing the current entry is often more convenient.
Keeping Sorting and Pagination Consistent
There is another important rule:
Changing the sort order should normally reset the page.
Imagine the user is on page 10 sorted by name.
They then sort by price.
The newly sorted data may have a completely different page structure.
Keeping page 10 after changing the sort can produce a confusing result.
A better approach is:
private void ChangeSort(string column)
{
if (SortColumn == column)
{
SortDescending = !SortDescending;
}
else
{
SortColumn = column;
SortDescending = false;
}
CurrentPage = 1;
UpdateGridUrl();
}
This gives predictable behavior.
Handling Invalid Page Numbers
URL parameters should always be treated as user input.
Suppose the URL contains:
/products?page=999999
but the database contains only 50 records.
The application should not blindly attempt to load that page.
After determining the total number of records, clamp the requested page to the valid range.
Conceptually:
var totalPages = (int)Math.Ceiling(
(double)TotalItems / PageSize);
CurrentPage = Math.Clamp(
CurrentPage,
1,
Math.Max(totalPages, 1));
This is particularly important when pagination is performed on the server.
Server-Side Data Is Different
For a small in-memory collection, client-side pagination is straightforward.
Production applications often have much larger datasets.
For example:
10,000 products
1,000,000 orders
50,000,000 log records
Loading everything into the browser just to display one page is inefficient.
Instead, the application should pass the requested page and sorting information to the data layer.
For example:
public async Task<List<Product>> GetProductsAsync(
int page,
int pageSize,
string sortColumn,
bool descending)
{
IQueryable<Product> query = _context.Products;
query = sortColumn switch
{
"name" => descending
? query.OrderByDescending(x => x.Name)
: query.OrderBy(x => x.Name),
"price" => descending
? query.OrderByDescending(x => x.Price)
: query.OrderBy(x => x.Price),
"category" => descending
? query.OrderByDescending(x => x.Category)
: query.OrderBy(x => x.Category),
_ => query.OrderBy(x => x.Id)
};
return await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
The switch is intentionally explicit.
Do not directly concatenate a URL-provided column name into SQL.
Instead, map allowed URL values to known properties.
This gives you a controlled list of sortable fields.
URL State With Server-Side Pagination
The complete flow can look like this:
/products?page=4&sort=price&direction=desc
|
v
Blazor Component
|
v
Validate parameters
|
v
Data Service
|
v
Database Query
|
v
Return requested records
|
v
QuickGrid
This approach scales much better than loading an entire large table into memory.
Comparison: Component State vs URL State
Feature | Component State | URL State |
|---|---|---|
Browser refresh | State may reset | State can be restored |
Bookmarking | Poor | Good |
Sharing a view | Poor | Good |
Browser history | Limited | Supported |
Implementation | Simpler | Requires additional code |
User experience | Good for temporary state | Better for shareable views |
Large applications | Useful | Often preferable for navigable views |
Neither approach is universally better.
Use component state when the state is temporary and does not need to be shared.
Use URL state when the state represents a meaningful view that users may want to revisit.
Common Mistakes
Putting Too Much State in the URL
Do not put every UI property into the query string.
For example, a table does not need URL parameters for every temporary visual setting.
Keep only state that is useful for navigation, sharing, or restoration.
Trusting Query Parameters
Never assume values from the URL are valid.
Validate:
page
sort
direction
pageSize
filters
before using them.
Allowing Arbitrary Sort Columns
Avoid accepting any string and trying to construct a database query dynamically.
Use an explicit mapping:
private static readonly Dictionary<string, string> AllowedSortColumns =
new()
{
["name"] = "Name",
["price"] = "Price",
["category"] = "Category"
};
The actual implementation can map these values directly to LINQ expressions.
Forgetting to Reset Pagination
When sorting or filtering changes, the current page may no longer be valid.
Reset it when appropriate.
Creating Too Many History Entries
If every grid interaction creates a history entry, browser navigation can become frustrating.
Consider replacing the current URL for rapid state changes.
Troubleshooting
The URL Changes but the Grid Does Not
Updating the URL does not automatically mean your component has applied the new state.
Make sure the component reads the query parameters and reloads the data when the relevant state changes.
Refreshing the Page Loses the State
Check that the component reads the query string during initialization or parameter processing.
The URL is useful only if the application can reconstruct the state from it.
Page Number Is Invalid
Validate the value and compare it with the actual number of available pages.
Do not blindly use a page number supplied by the browser.
Sorting Produces Unexpected Results
Check that the URL values map only to known sortable properties.
Also make sure the database query applies sorting before Skip() and Take().
Best Practices
Keep the URL format simple.
Use meaningful query parameter names.
Validate every URL parameter.
Use an allow-list for sortable columns.
Reset pagination when sorting or filtering changes.
Consider replacing browser history entries for frequent grid changes.
Use server-side pagination for large datasets.
Apply sorting before pagination.
Do not expose unnecessary internal database details in the URL.
Keep the URL state limited to information users may reasonably want to bookmark or share.
Advantages
Using the URL for QuickGrid state provides several practical benefits:
Users can bookmark a specific table view.
Grid state survives a browser refresh.
Developers can share reproducible views with teammates.
Support teams can receive a URL that contains the relevant page and sort state.
Browser navigation becomes more useful.
Server-side data loading can directly use the requested page and sort values.
Disadvantages
There are some trade-offs as well:
The component requires more code.
Query-string validation becomes necessary.
Complex filter state can make URLs difficult to read.
URL state can expose information that should not be placed in a URL.
Developers must keep URL parameters and component state synchronized.
For sensitive information, do not place private data in query strings.
Summary
QuickGrid handles the table itself, but the application still needs to decide how long the user's grid state should live.
If pagination and sorting are kept only inside the component, that state can disappear after a refresh or when the user leaves the page. Putting useful grid state in the URL solves that problem.
A URL such as:
/products?page=3&sort=price&direction=desc
makes the current view easy to refresh, bookmark, and share.
For production applications, the important part is not just updating the URL. Validate the values, allow only supported sort fields, reset pagination when the sort or filter changes, and use server-side pagination when the dataset is large.
With these practices, QuickGrid can provide a much better experience without requiring a complicated state-management system.

Join the conversation! Your thoughts help the community grow.