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:

  1. Opens the product page.

  2. Moves to page 4.

  3. Sorts by price.

  4. Finds the records they need.

  5. Copies the URL.

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

page

Current page number

sort

Column being sorted

direction

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

  1. Keep the URL format simple.

  2. Use meaningful query parameter names.

  3. Validate every URL parameter.

  4. Use an allow-list for sortable columns.

  5. Reset pagination when sorting or filtering changes.

  6. Consider replacing browser history entries for frequent grid changes.

  7. Use server-side pagination for large datasets.

  8. Apply sorting before pagination.

  9. Do not expose unnecessary internal database details in the URL.

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