Query Parameters

Track Filters & Pagination in the URL

Query Parameters

Declare query params on a controller (or route) so changes update the URL — and the URL re-hydrates state on reload or share.

4 min read Level 3/5 #ember#routing#queryparams
What you'll learn
  • Declare queryParams on a controller/route
  • Bind to `@tracked` properties
  • Use refreshModel to re-run model() on change

Query parameters keep UI state — filters, sort order, pagination — synced with the URL. Reload the page and you are back where you left off. Share the URL and the recipient sees the same view.

Declare on the Controller

// app/controllers/posts.js
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';

export default class PostsController extends Controller {
  queryParams = ['page', 'q', 'sort'];

  @tracked page = 1;
  @tracked q = '';
  @tracked sort = 'newest';
}

The queryParams array lists which properties sync to the URL. Defaults set above (page = 1) are not put in the URL — only deviations are serialized.

Use Them in the Template

{{! app/templates/posts.hbs }}
<input
  value={{this.q}}
  {{on "input" (fn (mut this.q) value="target.value")}}
  placeholder="Search"
/>

<select {{on "change" (fn (mut this.sort) value="target.value")}}>
  <option value="newest">Newest</option>
  <option value="oldest">Oldest</option>
</select>

Typing in the input updates this.q, which updates the URL.

Refresh the Model on Change

// app/routes/posts.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';

export default class PostsRoute extends Route {
  @service store;

  queryParams = {
    q: { refreshModel: true },
    sort: { refreshModel: true },
    page: { refreshModel: true },
  };

  async model(params) {
    return this.store.query('post', params);
  }
}

refreshModel: true re-runs model() whenever that query param changes — perfect for server-side search and pagination.

replace Option

queryParams = {
  q: { replace: true },
};

replace: true uses history.replaceState instead of pushState, so each keystroke does not add a history entry.

Route Actions →