Skip to content

Navigation Guards

Navigation Guards são funções que interceptam navegações antes que elas aconteçam. Você pode usá-los para:

  • ✅ Autenticação: Proteger rotas que exigem login
  • ✅ Autorização: Verificar permissões do usuário
  • ✅ Validação: Confirmar se dados necessários existem
  • ✅ Redirecionamento: Enviar usuários para outras rotas
  • ✅ Confirmação: Perguntar antes de sair de uma página (ex: formulário não salvo)
  • ✅ Analytics: Registrar navegações
type NavigationGuard = (
to: RouteMatch, // Rota de destino
from: RouteMatch | null // Rota atual (null se primeira navegação)
) => void | boolean | string | Promise<void | boolean | string>
RetornoComportamento
void ou truePermite a navegação
falseBloqueia a navegação
stringRedireciona para o path retornado
Promise<...>Aguarda resolução e aplica a regra acima

Guards globais são aplicados a todas as rotas:

import { createRouter } from '@_bashell/slash'
const router = createRouter({
routes: [
{ path: '/', component: Home },
{ path: '/dashboard', component: Dashboard }
],
guards: [
// Guard global: verificar autenticação
async (to, from) => {
const isAuthenticated = await checkAuth()
if (!isAuthenticated && to.path !== '/login') {
console.log('Usuário não autenticado, redirecionando...')
return '/login'
}
// Permitir navegação
return true
}
]
})

Guards específicos aplicam-se apenas à rota onde foram definidos:

import { createRouter } from '@_bashell/slash'
const router = createRouter({
routes: [
{
path: '/',
component: Home
},
{
path: '/admin',
component: AdminPanel,
guards: [
// Guard específico: verificar se é admin
async (to, from) => {
const user = await getCurrentUser()
if (!user || !user.isAdmin) {
console.log('Acesso negado: não é admin')
return '/' // Redirecionar para home
}
return true
}
]
},
{
path: '/settings',
component: Settings,
guards: [
// Guard de confirmação
(to, from) => {
if (from && hasUnsavedChanges()) {
const confirmed = confirm('Você tem alterações não salvas. Deseja sair?')
return confirmed // true permite, false bloqueia
}
return true
}
]
}
]
})

No navegador, a navegação inicial passa pelos mesmos guards (globais e da rota), com redirects, e usa replace (não empilha histórico). router.ready é uma Promise<void> que resolve quando ela termina.

  • URL do navegador (window.location, ou location.hash em mode: 'hash'): com guard aplicável, o estado inicial é currentRoute: null, isNavigating: true até a decisão, então nada protegido é renderizado antes. Depois vem a rota, o destino do redirect ou, se o guard bloquear, currentRoute: null.
  • initialPath e hidratação: a rota é aplicada de forma síncrona (o markup do SSR é mantido) e os guards rodam em seguida; se bloquearem, currentRoute vira null; se redirecionarem, o roteador segue o redirect.
  • SSR (sem navegador): os guards não rodam.
  • Cada navegação segue no máximo 10 redirects; acima disso o roteador registra um erro e deixa currentRoute: null. Uma navegação mais nova substitui uma pendente.
await router.ready // navegação inicial concluída

Guards são executados nesta ordem:

  1. Guards globais (definidos em RouterConfig.guards)
  2. Guards da rota (definidos em RouteConfig.guards)

Se qualquer guard bloquear ou redirecionar, os próximos não executam.

const router = createRouter({
routes: [
{
path: '/protected',
component: ProtectedPage,
guards: [
// 2️⃣ Executa depois do guard global
(to, from) => {
console.log('Guard da rota executando...')
return true
}
]
}
],
guards: [
// 1️⃣ Executa primeiro
(to, from) => {
console.log('Guard global executando...')
return true
}
]
})
import { createRouter } from '@_bashell/slash'
// Simulação de auth
let isLoggedIn = false
function login() {
isLoggedIn = true
}
function logout() {
isLoggedIn = false
}
const router = createRouter({
routes: [
{
path: '/',
component: () => html`<h1>Home</h1>`
},
{
path: '/login',
component: () => html`
<div>
<h1>Login</h1>
<button onClick=${() => {
login()
router.push('/dashboard')
}}>
Entrar
</button>
</div>
`
},
{
path: '/dashboard',
component: () => html`
<div>
<h1>Dashboard</h1>
<button onClick=${() => {
logout()
router.push('/')
}}>
Sair
</button>
</div>
`,
guards: [
(to, from) => {
if (!isLoggedIn) {
return '/login'
}
return true
}
]
}
]
})
type User = {
id: number
name: string
role: 'admin' | 'user' | 'guest'
}
let currentUser: User | null = null
const requireRole = (requiredRole: User['role']) => {
return (to: RouteMatch, from: RouteMatch | null) => {
if (!currentUser) {
return '/login'
}
if (currentUser.role !== requiredRole) {
console.warn(`Acesso negado: requer role ${requiredRole}`)
return '/'
}
return true
}
}
const router = createRouter({
routes: [
{
path: '/admin',
component: AdminPanel,
guards: [requireRole('admin')]
},
{
path: '/user-profile',
component: UserProfile,
guards: [requireRole('user')]
}
]
})
import { createRouter } from '@_bashell/slash'
import { createState } from '@_bashell/slash'
const userData = createState<any>(null)
const router = createRouter({
routes: [
{
path: '/profile/:id',
component: (state) => {
const user = userData.get()
if (!user) {
return html`<p>Carregando...</p>`
}
return html`
<div>
<h1>${user.name}</h1>
<p>${user.email}</p>
</div>
`
},
guards: [
async (to, from) => {
const userId = to.params.id
try {
// Carregar dados do usuário antes de navegar
const response = await fetch(`/api/users/${userId}`)
const user = await response.json()
userData.set(user)
return true // Permitir navegação
} catch (error) {
console.error('Erro ao carregar usuário:', error)
return '/' // Redirecionar para home em caso de erro
}
}
]
}
]
})
const router = createRouter({
routes: [
{ path: '/', component: Home },
{ path: '/about', component: About }
],
guards: [
// Guard global para analytics
(to, from) => {
// Enviar evento de pageview
if (typeof gtag !== 'undefined') {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: to.path
})
}
console.log('Navegação:', {
from: from?.path || 'inicial',
to: to.path,
params: to.params,
query: to.query
})
// Sempre permitir navegação
return true
}
]
})
import { createState } from '@_bashell/slash'
const hasUnsavedChanges = createState(false)
let content = '' // texto em edição: não é lido no render, então o <textarea> não é recriado
function Editor({ router }) {
const handleInput = (e: Event) => {
content = (e.target as HTMLTextAreaElement).value
hasUnsavedChanges.set(true)
}
const handleSave = () => {
// Salvar conteúdo...
hasUnsavedChanges.set(false)
console.log('Conteúdo salvo!')
}
return html`
<div>
<h1>Editor</h1>
<textarea onInput=${handleInput}></textarea>
<button onClick=${handleSave}>Salvar</button>
</div>
`
}
const router = createRouter({
routes: [
{
path: '/editor',
component: Editor,
guards: [
(to, from) => {
// Guard de saída: confirmar se há mudanças não salvas
if (from && from.path === '/editor' && hasUnsavedChanges.get()) {
const confirmed = confirm(
'Você tem alterações não salvas. Deseja sair mesmo assim?'
)
return confirmed
}
return true
}
]
}
]
})

Guards podem ser assíncronos para fazer verificações que dependem de I/O:

const router = createRouter({
routes: [
{
path: '/premium',
component: PremiumContent,
guards: [
async (to, from) => {
// Verificar status de assinatura no servidor
const response = await fetch('/api/subscription/status')
const { isPremium } = await response.json()
if (!isPremium) {
return '/upgrade' // Redirecionar para página de upgrade
}
return true
}
]
}
]
})

Se um guard lançar um erro, a navegação é bloqueada:

const router = createRouter({
routes: [
{
path: '/dashboard',
component: Dashboard,
guards: [
async (to, from) => {
try {
const user = await fetchCurrentUser()
return !!user
} catch (error) {
console.error('Erro no guard:', error)
// Erro = navegação bloqueada
// Se quiser redirecionar em caso de erro, use return '/error'
return false
}
}
]
}
]
})

Você pode usar metadados de rota (meta) para controle declarativo:

const router = createRouter({
routes: [
{
path: '/',
component: Home,
meta: { requiresAuth: false }
},
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true, requiredRole: 'user' }
},
{
path: '/admin',
component: AdminPanel,
meta: { requiresAuth: true, requiredRole: 'admin' }
}
],
guards: [
// Guard global que lê metadata
(to, from) => {
const { requiresAuth, requiredRole } = to.meta
if (requiresAuth && !isLoggedIn()) {
return '/login'
}
if (requiredRole && !hasRole(requiredRole)) {
return '/'
}
return true
}
]
})
  1. Mantenha guards simples: Lógica complexa deve estar em serviços/helpers
  2. Use metadados: Prefira metadados + guard global ao invés de guards duplicados
  3. Trate erros: Sempre use try/catch em guards assíncronos
  4. Evite side effects: Guards devem ser funções puras quando possível
  5. Documente comportamento: Deixe claro o que cada guard faz e por que