Creando un componente <Route> declarativo

En esta clase aprenderás a transformar un router con lógica imperativa (basado en if y switch) en un sistema declarativo usando un componente <Route>. Este cambio mejorará drásticamente la legibilidad, escalabilidad y mantenibilidad de tu aplicación.

El problema: Lógica imperativa en el componente principal

Actualmente, nuestro router funciona con condicionales en el componente principal:

// App.jsx - Versión con if
import { useRouter } from './hooks/useRouter'
import { Home } from './pages/Home'
import { Search } from './pages/Search'
import { About } from './pages/About'

function App() {
  const { currentPath } = useRouter()

  // ❌ Problema: Lógica imperativa con if
  if (currentPath === '/') {
    return <Home />
  }

  if (currentPath === '/search') {
    return <Search />
  }

  if (currentPath === '/about') {
    return <About />
  }

  return <NotFound />
}

export default App

Alternativa con switch (tampoco es ideal)

Alguien podría sugerir usar switch para mejorar el código:

// App.jsx - Versión con switch
function App() {
  const { currentPath } = useRouter()

  // ❌ Sigue siendo imperativo
  switch (currentPath) {
    case '/':
      return <Home />
    case '/search':
      return <Search />
    case '/about':
      return <About />
    default:
      return <NotFound />
  }
}

¿Por qué no es ideal?

Aunque switch está mejor que múltiples if, ambos enfoques tienen problemas:

Lógica imperativa: Describes cómo funciona el routing, no qué debe hacer ❌ No escala bien: Añadir rutas significa modificar la lógica del componente ❌ Difícil de leer: No es evidente qué rutas existen de un vistazo ❌ No es declarativo: No expresa la intención claramente ❌ Lógica acoplada: El componente principal conoce los detalles del routing

La solución: Componente <Route> declarativo

En lugar de condicionales, vamos a crear un componente <Route> que encapsule la lógica de matching.

Ventajas del enfoque declarativo

Expresa intención: Cada ruta es clara y autoexplicativa ✅ Fácil de leer: Ves todas las rutas de un vistazo ✅ Escalable: Añadir rutas es solo añadir más <Route>Separación de responsabilidades: Cada <Route> gestiona su propia lógica ✅ Sin condicionales: El componente principal queda limpio

Creando el componente <Route>

Paso 1: Crear el archivo del componente

Vamos a crear un nuevo archivo para nuestro componente:

// components/Route.jsx
import { useRouter } from '../hooks/useRouter'

export function Route({ path, component: Component }) {
  // Obtener la ruta actual del router
  const { currentPath } = useRouter()

  // Si la ruta no coincide, no renderizar nada
  if (currentPath !== path) {
    return null
  }

  // Si coincide, renderizar el componente
  return <Component />
}

Desglosando el código

1. Reutilizamos el custom hook useRouter()

const { currentPath } = useRouter()

Aquí es donde se ve claramente el poder del custom hook que creamos en la clase anterior:

  • ✅ Centraliza la lógica del router
  • ✅ Se puede reutilizar en diferentes componentes
  • ✅ Mantiene el código limpio y organizado

2. Decisión de renderizado

if (currentPath !== path) {
  return null
}

Este es el patrón de renderizado condicional en React:

  • Si la ruta actual NO coincide con la ruta del componente → no renderiza nada (null)
  • Si coinciden → renderiza el componente

3. Prop component con destructuring

export function Route({ path, component: Component }) {
  return <Component />
}

Usamos destructuring con renaming:

  • component: Component → Recibimos component pero lo renombramos a Component (con mayúscula)
  • ¿Por qué? En JSX, los componentes deben empezar con mayúscula
  • Esto nos permite hacer <Component /> en lugar de <component />

Usando el componente <Route> en App

Antes (imperativo)

// App.jsx - ANTES
import { useRouter } from './hooks/useRouter'
import { Home } from './pages/Home'
import { Search } from './pages/Search'
import { About } from './pages/About'

function App() {
  const { currentPath } = useRouter()

  if (currentPath === '/') return <Home />
  if (currentPath === '/search') return <Search />
  if (currentPath === '/about') return <About />

  return <NotFound />
}

Después (declarativo)

// App.jsx - DESPUÉS
import { Route } from './components/Route'
import { Home } from './pages/Home'
import { Search } from './pages/Search'
import { About } from './pages/About'
import { NotFound } from './pages/NotFound'

function App() {
  return (
    <>
      <Route path="/" component={Home} />
      <Route path="/search" component={Search} />
      <Route path="/about" component={About} />
      <Route path="/404" component={NotFound} />
    </>
  )
}

export default App

¿Qué cambió?

Sin condicionales: Nada de if, switch o ternarios ✅ Declarativo: Cada línea declara una ruta claramente ✅ Auto-gestionado: Cada <Route> decide si debe renderizarse ✅ Escalable: Añadir rutas es solo añadir más <Route>Legible: Ves todas las rutas de un vistazo

Cómo funciona internamente

Vamos a entender qué pasa cuando renderizamos estas rutas:

<>
  <Route path="/" component={Home} />
  <Route path="/search" component={Search} />
  <Route path="/about" component={About} />
</>

Si estamos en /

  1. Primera ruta (path="/")

    • currentPath === "/" → ✅ Coincide
    • Renderiza <Home />
  2. Segunda ruta (path="/search")

    • currentPath !== "/search" → ❌ No coincide
    • Retorna null (no renderiza nada)
  3. Tercera ruta (path="/about")

    • currentPath !== "/about" → ❌ No coincide
    • Retorna null (no renderiza nada)

Resultado final: Solo se renderiza <Home />

  1. Primera ruta (path="/")

    • currentPath !== "/" → ❌ No coincide
    • Retorna null
  2. Segunda ruta (path="/search")

    • currentPath === "/search" → ✅ Coincide
    • Renderiza <Search />
  3. Tercera ruta (path="/about")

    • currentPath !== "/about" → ❌ No coincide
    • Retorna null

Resultado final: Solo se renderiza <Search />

Patrón de renderizado con null

Renderizar null es un patrón muy común en React:

function ConditionalComponent({ shouldShow, children }) {
  // Si no debe mostrarse, renderiza null
  if (!shouldShow) {
    return null
  }

  // Si debe mostrarse, renderiza el contenido
  return <div>{children}</div>
}

Ventajas:

Limpio: No ensucia el DOM con elementos vacíos

Performante: React optimiza el renderizado de null

Idiomático: Es el estándar en React para “no renderizar nada”

Comparación: Imperativo vs Declarativo

Imperativo (con if)

function App() {
  const { currentPath } = useRouter()

  // Describes CÓMO decidir qué renderizar
  if (currentPath === '/') {
    return <Home />
  } else if (currentPath === '/search') {
    return <Search />
  } else if (currentPath === '/about') {
    return <About />
  } else {
    return <NotFound />
  }
}

Desventajas:

  • Lógica imperativa (“si esto, entonces aquello”)
  • El componente principal conoce todos los detalles
  • No es fácilmente extensible
  • Difícil de testear cada ruta independientemente

Declarativo (con <Route>)

function App() {
  // Declaras QUÉ debe pasar en cada ruta
  return (
    <>
      <Route path="/" component={Home} />
      <Route path="/search" component={Search} />
      <Route path="/about" component={About} />
      <Route path="/404" component={NotFound} />
    </>
  )
}

Ventajas:

  • Declarativo (“en esta ruta, este componente”)
  • Cada ruta es autónoma
  • Fácil añadir/quitar rutas
  • Fácil de testear cada <Route> independientemente

Añadiendo un layout compartido

Ahora que tenemos un sistema declarativo, es muy fácil añadir elementos compartidos:

function App() {
  return (
    <>
      <Header />

      <main>
        <Route path="/" component={Home} />
        <Route path="/search" component={Search} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </main>

      <Footer />
    </>
  )
}

Lo que pasa:

  • El <Header /> siempre se renderiza
  • Solo una de las rutas se renderiza (la que coincide)
  • El <Footer /> siempre se renderiza

Próximos pasos

Con este patrón declarativo, hemos sentado las bases para un router profesional. En las próximas clases aprenderemos:

  1. Rutas dinámicas: /users/:id
  2. Rutas anidadas: Rutas dentro de rutas
  3. Protección de rutas: Rutas privadas que requieren autenticación
  4. Navegación programática: Navegar desde eventos
  5. React Router: Migrar a la librería oficial

Conceptos clave aprendidos

ConceptoDescripción
Programación declarativaExpresar qué queremos, no cómo lograrlo
Programación imperativaDescribir paso a paso cómo hacer algo
Renderizado condicionalComponentes que deciden si renderizar o no
Retornar nullPatrón para indicar “no renderizar nada”
Composición de componentesCrear componentes que usan otros componentes
Props con renamingcomponent: Component para usar mayúsculas
Reutilización de hooksuseRouter() usado en múltiples lugares

Ventajas del patrón <Route>

VentajaDescripción
🎯 DeclarativoExpresa claramente la intención
📖 LegibleFácil de entender de un vistazo
🔧 MantenibleCambios localizados en cada ruta
📈 EscalableAñadir rutas no complica el código
🧩 ModularCada ruta es independiente
TesteableFácil testear cada ruta por separado
🔄 ReutilizableEl componente <Route> es reusable

Conclusión

En esta clase has aprendido a:

  • ✅ Identificar problemas de código imperativo con if y switch
  • ✅ Crear un componente <Route> declarativo y reutilizable
  • ✅ Reutilizar el custom hook useRouter() en diferentes componentes
  • ✅ Usar el patrón de renderizado condicional con null
  • ✅ Aplicar programación declarativa en React
  • ✅ Entender cómo funciona internamente el routing declarativo
  • ✅ Crear una base sólida para un sistema de routing escalable

Este patrón declarativo es la base de cómo funcionan los routers profesionales como React Router. Has construido una versión simplificada pero completamente funcional que te ayudará a entender los conceptos fundamentales del routing en aplicaciones web modernas.

💡 Recuerda: En producción siempre debes usar React Router o una librería similar, pero entender cómo funciona internamente te hace un mejor desarrollador.