Sincronizar filtros na URL com Angular: Signals + Router + RxJS (para iniciantes)
Por John Pires · 16 de março de 2026

Por que aprender isso em 2026
Contexto técnico e relevância
Em 2026 os applications web continuam combinando experiência reativa com URLs que representam estado. URLs serializam filtros, mantêm navegabilidade e permitem compartilhar telas. Entender como manter filtros sincronizados com a URL evita estados inconsistentes, melhora SEO parcial, permite bookmarking e torna aplicações previsíveis.
Analogia simples
Pense na URL como o painel frontal de um carro que mostra a velocidade e o combustível — os filtros são o motor. O painel (URL) deve refletir o que o motor (estado da UI) está fazendo. Se um mecânico (o código) mexe no motor, o painel precisa atualizar; se alguém gira o painel (muda a URL manualmente), o motor precisa reagir. Sincronizar filtros é sobre garantir que painel e motor sigam a mesma versão da verdade.
Este artigo explora por que Signals (estado reativo nativo do Angular) + Router + RxJS formam uma combinação sólida: Signals para estado local, Router para persistência na URL, RxJS para debouncing, comparação e evitar loop de navegação.
Configuração do ambiente
Requisitos mínimos
Você precisa ter Node.js (>=18 recomendado), Angular CLI (>=16) e um projeto Angular standalone ou tradicional. Vamos usar APIs modernas: signals (signal, effect), inject(), Router e ActivatedRoute. RxJS será usado para operadores como debounceTime, distinctUntilChanged e Subject.
Criando um projeto rápido
Passos práticos para iniciar um projeto standalone. Abra o terminal e execute:
npm init @angular/app my-filters-app -- --standaloneO comando acima gera um projeto com components standalone. Se preferir nova versão do CLI, use ng new com flags apropriadas.
Dependências que vamos usar
Além do Angular padrão, você precisa apenas do rxjs (vem com Angular). Certifique-se das versões compatíveis no package.json. Não usaremos bibliotecas externas para o gerenciamento de query params — o Router nativo é suficiente.
Hello World progressivo
Objetivo do exemplo
Construiremos um pequeno componente que:
Tem um campo de busca (search).
Sincroniza o valor com a query string (ex: ?search=term).
Evita loop de navegação e aplica debounce em digitação.
Componente mínimo com signals e router
Contexto: este componente standalone demonstra sincronização unidirecional: URL -> signal e signal -> URL com proteção contra loops. Leia o código, depois explico linha a linha.
import { Component, signal, effect, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@Component({
selector: 'app-search-sync',
standalone: true,
template: `
`
})
export class SearchSyncComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
search = signal('');
private updatingFromUrl = signal(false);
private write$ = new Subject();
constructor() {
// 1) Escuta a query param e atualiza o signal
this.route.queryParamMap.subscribe(map => {
const s = map.get('search') ?? '';
this.updatingFromUrl.set(true);
this.search.set(s);
this.updatingFromUrl.set(false);
});
// 2) Quando o signal muda, empurra para um Subject para debouncing
effect(() => this.write$.next(this.search()));
// 3) Fluxo RxJS que navega alterando a URL (debounced)
this.write$.pipe(debounceTime(300)).subscribe(value => {
if (this.updatingFromUrl()) return; // evita loop
this.router.navigate([], {
relativeTo: this.route,
queryParams: { search: value || null },
queryParamsHandling: 'merge'
});
});
}
onInput(val: string) {
this.search.set(val);
}
}
O que acontece: a subscription em queryParamMap atualiza o signal quando a URL muda. O effect empurra o novo valor em um Subject que passa por debounceTime antes de chamar router.navigate. A flag updatingFromUrl evita que o navigate re-dispare update desnecessário.
O que muda em cada passo
Quando o usuário digita, o input chama onInput que atualiza o signal. O effect empurra esse valor ao Subject e, após debounce, o subscriber chama router.navigate. Quando a URL muda (por qualquer motivo), a subscription em queryParamMap atualiza o signal sem reemitir navigation.
Mini-projeto prático: lista com filtros sincronizados
Requisitos do mini-projeto
Construiremos uma tela de listagem fictícia com filtros:
search (string)
tag (categoria) — string
page (número)
Objetivos concretos:
Inicializar filtros a partir da URL.
Atualizar URL quando filtros mudam.
Aplicar debounce no search e evitar loop entre URL <-> signals.
Mostrar como integrar com um serviço que retorna resultados (mock).
Modelo de estado e utilitários
Contexto: definimos tipos e funções utilitárias para serializar/limpar query params.
export interface Filters {
search: string;
tag: string | null;
page: number;
}
export function cleanQueryParams(f: Filters) {
// Remove valores vazios para evitar URLs poluídas
const qp: any = {};
if (f.search) qp.search = f.search;
if (f.tag) qp.tag = f.tag;
if (f.page && f.page !== 1) qp.page = f.page;
return qp;
}
O que faz: a função cleanQueryParams converte o estado em query params, omitindo valores padrão. Isso mantém a URL curta e previsível.
Componente completo do mini-projeto
Contexto: o componente abaixo junta tudo: signals, Router, ActivatedRoute, RxJS e um serviço mock que devolve itens filtrados. Cole e rode dentro de um projeto Angular standalone.
import { Component, signal, effect, computed, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Subject, of } from 'rxjs';
import { debounceTime, switchMap } from 'rxjs/operators';
// Simula serviço que retorna itens
function fetchItems(filters: { search: string; tag: string | null; page: number; }) {
const all = Array.from({ length: 50 }).map((_, i) => ({ id: i + 1, title: `Item ${i + 1}`, tag: i % 2 ? 'a' : 'b' }));
const filtered = all
.filter(x => !filters.search || x.title.toLowerCase().includes(filters.search.toLowerCase()))
.filter(x => !filters.tag || x.tag === filters.tag);
const pageSize = 10;
const start = (filters.page - 1) * pageSize;
return of({ items: filtered.slice(start, start + pageSize), total: filtered.length });
}
@Component({
selector: 'app-list-filters',
standalone: true,
template:
all
a
b
prev
next
Total: {{ result()?.total }}{{ item.title }} ({{ item.tag }})
})
export class ListFiltersComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
// estado local como signal
filters = signal({ search: '', tag: null, page: 1 });
private fromUrl = signal(false);
// stream para debouncing de navegação
private write$ = new Subject();
// resultado computado (poderia ser um signal derivado de uma chamada real)
result = signal<{ items: any[]; total: number } | null>(null);
constructor() {
// inicializa a partir da URL
this.route.queryParamMap.subscribe(map => {
const f: Filters = {
search: map.get('search') ?? '',
tag: map.get('tag'),
page: +(map.get('page') ?? 1)
};
this.fromUrl.set(true);
this.filters.set(f);
this.fromUrl.set(false);
});
// quando filters muda, empurra para rx stream
effect(() => this.write$.next(this.filters()));
// debouce e atualiza URL sem loop
this.write$.pipe(debounceTime(250)).subscribe(f => {
if (this.fromUrl()) return;
this.router.navigate([], { relativeTo: this.route, queryParams: cleanQueryParams(f), queryParamsHandling: 'merge' });
});
// sempre que filters mudar, buscar itens (ex: efeito que chama serviço)
effect(() => {
const f = this.filters();
// fetchItems retorna Observable; usamos switchMap-like pattern simplificado
fetchItems(f).subscribe(res => this.result.set(res));
});
}
setSearch(val: string) { this.filters.update(s => ({ ...s, search: val, page: 1 })); }
setTag(val: string | null) { this.filters.update(s => ({ ...s, tag: val, page: 1 })); }
prevPage() { this.filters.update(s => ({ ...s, page: Math.max(1, s.page - 1) })); }
nextPage() { this.filters.update(s => ({ ...s, page: s.page + 1 })); }
}
O que acontece: o componente lê query params no construtor e popula o signal. A cada mudança local no signal, o efeito empurra os filtros para o Subject que aplica debounce antes de chamar router.navigate. Outro efeito consome o state e chama fetchItems para atualizar a lista mostrada.
Erros comuns e armadilhas
Contexto: aqui estão várias pegadinhas que iniciantes encontrarão ao sincronizar estado e URL.
Loop de navegação: chamar router.navigate em resposta a query params sem sinalizar a origem gera loops infinitos. Use uma flag (como fromUrl/updatingFromUrl) ou compare valores antes de navegar.
Poluição da URL: não remova valores padrão. Use funções que limpem query params antes de navegar para evitar URLs cheias de parâmetros vazios.
Debounce e UX: não debounce por muito tempo. 200–300ms é prática comum para search inputs.
Comparação profunda: para objetos complexos você precisa comparar profundamente antes de navegar. Operators como distinctUntilChanged com isEqual ajudam no RxJS.
Dica: teste manualmente atualizando a query string no navegador e observando se a UI reflete o estado. Ferramentas como o Redux DevTools ajudam quando o state está centralizado — para signals, console.log dos sinais funciona bem em dev.
Recursos para continuar estudando
Documentação e guias oficiais
Leia a documentação do Angular sobre Signals, Router e standalone components. Os guias oficiais mostram patterns recomendados e limitações. Complementar com a documentação do RxJS para entender operadores como debounceTime, distinctUntilChanged, switchMap.
Tópicos avançados para seguir
Depois de dominar a sincronização básica, estude:
toSignal / toObservable (interop entre Signals e Observables) para simplificar conversões.
Gerenciamento de estado com signals e stores locais (signal stores).
Estratégias de serialização de filtros complexos (arrays, objetos) para query params.
Conclusão prática
Pontos-chave
Sincronizar filtros com a URL melhora previsibilidade e compartilhabilidade da aplicação. A combinação de signals (estado local), Router (persistência na URL) e RxJS (debounce, comparação, composição) resolve a maioria dos casos sem bibliotecas externas.
Passos práticos finais
Resumo de passos aplicáveis agora:
Defina um estado (signal) que represente seus filtros.
Leia query params no construtor e atualize o signal (marque origem pra evitar loop).
Empurre mudanças do signal para um Subject; aplique debounce e distinct antes de chamar router.navigate.
Use cleanQueryParams para evitar parâmetros desnecessários na URL.
Implementando essas etapas você terá filtros que persistem na URL, reagem a mudanças manuais da barreira do navegador e evitam loops de navegação. Para iniciantes: comece pequeno (um filtro), depois escale para múltiplos parâmetros e tipos mais complexos.
Este artigo forneceu código funcional, explicações e dicas práticas para aplicar imediatamente. A abordagem proposta é incremental, segura contra loops e compatível com aplicações modernas em Angular.
Assuntos: Angular, signals, rxjs, router, standalone
Leia também
Wizard multi-step com rotas filhas, Signals e Standalone Components
Tutorial prático e direto para criar um wizard multi-step em Angular: rotas filhas para cada etapa, gerenciamento de estado com Signals e componentes…
Autenticação reativa com Guards, Signals e Standalone Components em Angular (passo a passo)
Tutorial prático para iniciantes: implemente autenticação reativa em Angular usando signals, standalone components e guards. Passo a passo com exemplos…
Cancelar requisições ao navegar: AbortController, Signals e RxJS em componentes standalone Angular
Padrões práticos para cancelar requisições HTTP ao navegar em Angular: combinar AbortController, rxjs/fromFetch e signals em components standalone para evitar…
Providers com escopo de rota em Angular standalone: signals, ciclo de vida e limpeza
Guia prático para usar providers com escopo de rota em Angular standalone: exemplos com Signals, factories que leem params de rota, limpeza via onCleanup e…
Signals + RxJS em rotas lazy com Standalone Components e Route Providers: modo errado vs modo certo
Padrões práticos para combinar Signals e RxJS em rotas lazy com standalone components e route providers: comparativo, erros comuns e implementação correta…
Prefetch de dados por rota no Angular com Standalone Components e Signals
Aprenda a prefetch de dados por rota no Angular usando Standalone Components e Signals: combine ResolveFn com um PrefetchStore reativo para reduzir latência…