Managing Filters, Pagination, And Wizards With SwapState

Swap.Htmx Team February 15, 2026 3 min read
swap.htmx swapstate pagination filters aspnetcore

UI state is where many otherwise simple apps become harder than expected.

At first it is just one filter. Then it is sorting, paging, selected tabs, validation progress, and multi-step forms. None of these are hard alone. The problem is keeping them consistent across requests and keeping the browser, server, and URLs in agreement.

SwapState is useful because it keeps that coordination server-owned.

What kind of state we are talking about

Not all state belongs in the same place. SwapState is a strong fit for interface state such as:

  • current filters,
  • page number and page size,
  • selected step in a wizard,
  • sort direction,
  • the current view mode.

This is state that shapes how the server renders the screen. That is why the server is often the cleanest owner.

Why client-side stores become tempting

Teams often reach for a frontend store because the UI state is spread across several controls and updates. That can work, but it also means:

  • more synchronization logic,
  • more rules about serialization and deserialization,
  • more chances for the browser and server to disagree.

With server-driven UI, the server already has the final say on what the next rendered state should look like. SwapState leans into that instead of working around it.

Common use cases

Filtered lists

When a user changes a filter, the server can rebuild the results, counters, summary text, and pagination controls from the same authoritative state.

Paged screens

Page changes should preserve the active filter and sort state automatically. This is where hidden state and encoded route/query values become useful.

Wizards

Multi-step flows often look simple until users move backward, refresh, or partially complete a step. Server-owned state makes those transitions easier to reason about.

A real example from this site

The state demo uses [FromSwapState] in the controller and a <swap-state> tag helper in the view so filtering and pagination keep working across requests without a separate browser-side store.

[HttpGet("filter")]
public IActionResult Filter([FromSwapState] ProductFilterState state)
{
	ModelState.Clear();
	var (products, totalCount) = FilterProducts(state);

	return PartialView("_ProductFilter", new ProductViewModel
	{
		State = state,
		Products = products,
		TotalCount = totalCount
	});
}

And the paired view keeps that state moving through the UI:

<swap-state state="Model.State" />

<input type="text"
	   name="Search"
	   value="@Model.State.Search"
	   hx-get="/demo/state/filter"
	   hx-target="#product-filter-container"
	   hx-include="#@Model.State.ContainerId"
	   hx-trigger="keyup changed delay:300ms" />

That combination is what makes the server-owned state model practical instead of theoretical.

A practical design rule

If the state exists mainly to determine what HTML the server should render next, keep it close to the server.

That reduces duplication and makes your controller and view logic easier to test. It also makes progressive enhancement more natural because the screen can degrade to normal request/response behavior more cleanly.

Next step