Controlando el Flujo con Conductores

Las herramientas para optimizar, controlar y ejecutar acciones en tu app.

Más Allá de la Reacción Automática

En la lección anterior, aprendimos que Shiny reacciona automáticamente a los cambios en los inputs. Esto es fantástico para la interactividad simple, pero en aplicaciones del mundo real, a menudo necesitamos más control. ¿Qué pasa si un cálculo es muy lento y no queremos que se ejecute con cada pequeño ajuste? ¿O si queremos realizar una acción, como guardar un archivo, sin necesidad de producir un output visible?

Aquí es donde entran en juego los conductores reactivos. Son las herramientas que nos permiten pasar de una reactividad simple y automática a un flujo de trabajo controlado, eficiente y profesional. Nos dan el poder de decidir cuándo y cómo se ejecuta nuestro código.

Explorando los Conductores Reactivos

Los conductores son tus herramientas más poderosas para orquestar el flujo reactivo. Hay tres principales que cubren casi todas las necesidades.

reactive({ ... }): El Trabajador Eficiente

¿Qué hace? Crea una expresión reactiva que actúa como un "componente intermedio". Calcula un valor y lo guarda en caché. Este cálculo solo se re-ejecuta si las fuentes reactivas de las que depende cambian. Si múltiples outputs piden su valor, pero nada ha cambiado, devuelve el resultado guardado sin volver a calcular. Para acceder a su resultado, siempre debes llamarlo como una función (ej., datos_filtrados()).

Cuándo usarlo: Es tu herramienta principal para evitar la duplicación de código (principio DRY). Úsalo siempre que un mismo cálculo (filtrar datos, ajustar un modelo simple) sea necesario para más de un output. Garantiza que el trabajo se haga una sola vez.

eventReactive({ ... }): El Trabajador Obediente

¿Qué hace? Es similar a reactive(), pero con una diferencia crucial: solo se re-ejecuta cuando una fuente reactiva específica que tú designas (típicamente un actionButton) es activada. Ignora los cambios en otras dependencias hasta que recibe la "señal" explícita para actuar.

Cuándo usarlo: Para procesos computacionalmente costosos o que no deben ejecutarse constantemente. Si tienes 10 sliders para configurar un modelo, no quieres que el modelo se re-entrene con cada mínimo movimiento. En su lugar, usas eventReactive() para que solo se ejecute cuando el usuario haga clic en un botón de "Analizar" o "Calcular".

observeEvent({ ... }): El Mensajero

¿Qué hace? A diferencia de los otros dos, este conductor no devuelve un valor para ser usado en otro lugar. Su único propósito es realizar una acción o "efecto secundario" cuando un evento ocurre. No produce un output, simplemente "hace algo".

Cuándo usarlo: Para tareas que no actualizan un gráfico o una tabla, como mostrar una notificación emergente (showNotification()), guardar datos en un archivo, imprimir mensajes de depuración en la consola (cat()), o actualizar dinámicamente los valores de otro control de input (updateSelectInput()).

Taller Práctico: Implementando Control en Nuestra App

Vamos a refactorizar nuestra aplicación de análisis del ICFES para que el usuario tenga el control total sobre cuándo se ejecuta el análisis.

Paso 1: Añadir un Botón de Acción a la UI

Primero, necesitamos el "gatillo" que iniciará el análisis. Abre el archivo de tu UI (ui.R o la sección `ui` de tu `app.R`) y añade un actionButton dentro del sidebarPanel, justo debajo de los selectores.

# ... dentro del sidebarPanel ...
      selectInput(
        inputId = "var_y",
        label = "Seleccione Puntaje Eje Y:",
        choices = puntajes_choices,
        selected = "punt_lectura_critica"
      ),
      
      # NUEVO BOTÓN
      actionButton(
        inputId = "run_analysis", 
        label = "Ejecutar Análisis", 
        icon = icon("play"),
        class = "btn-success" # Estilo de Bootstrap para un botón verde
      )
# ...

Paso 2: Usar eventReactive para Controlar el Cálculo

Ahora, en tu lógica de servidor (server.R o la función `server`), reemplaza el reactive() que tenías por un eventReactive(). El primer argumento de esta función es el input que lo activa (nuestro botón), y el segundo es el bloque de código que se ejecutará.

# Reemplaza tu conductor reactivo anterior con este:

# Este conductor ahora SÓLO se ejecutará cuando se presione input$run_analysis
datos_analisis <- eventReactive(input$run_analysis, {
  
  # Filtramos los datos usando los valores actuales de los selectores
  datos_saber %>%
    filter(cole_naturaleza == input$naturaleza_select)
})

# Asegúrate de que tus outputs ahora usen el nuevo conductor: datos_analisis()
output$scatter_plot <- renderPlotly({
  p <- ggplot(datos_analisis(), aes_string(x = input$var_x, y = input$var_y)) +
    # ... resto del código del gráfico ...
  ggplotly(p)
})

output$tabla_completa <- renderDataTable({
  datos_analisis()
})

Paso 3: Dar Feedback al Usuario con observeEvent

Para mejorar la experiencia, podemos notificar al usuario que el análisis ha comenzado. Usaremos un observeEvent que "escuche" al mismo botón y muestre un mensaje.

Añade este bloque de código en cualquier parte dentro de tu función `server`.

# Este observador se dispara cuando se hace clic en el botón
observeEvent(input$run_analysis, {
  
  # Muestra una notificación temporal en la esquina de la app
  showNotification("Generando análisis...", type = "message")
  
  # También podemos imprimir en la consola para depuración
  cat("Análisis iniciado a las:", format(Sys.time()), "\n")
})

¡Vuelve a ejecutar tu aplicación! Ahora notarás que puedes cambiar los selectores libremente, y el gráfico y la tabla solo se actualizarán cuando hagas clic en "Ejecutar Análisis", momento en el cual también aparecerá una notificación. ¡Has tomado el control total del flujo reactivo!

Paso 4: Versionar los Cambios

Has implementado una mejora fundamental en la funcionalidad y eficiencia de la aplicación. Es hora de guardarlo en Git.

# Añade los cambios en los archivos ui.R y server.R (o app.R)
git add .

# Crea un commit descriptivo
git commit -m "feat(analysis): Implementa control de ejecución con eventReactive y notificaciones"

# Sube los cambios a GitHub
git push

Apéndice: Desmitificando Errores Comunes

Entender estos errores es un paso clave para dominar el flujo de datos en tus aplicaciones. Veamos dos casos prácticos que ilustran conceptos fundamentales de la reactividad.

Error 1: El Bucle Infinito (Recursión Accidental)

Este es uno de los errores conceptuales más comunes. Ocurre cuando un conductor reactivo intenta usarse a sí mismo como fuente de datos, creando un ciclo que Shiny no puede resolver.

Código Problemático
# Se intenta crear un reactive que se llama a sí mismo
datos_saber_muestra <- reactive({
  if (input$naturaleza_colegio == "Todos") {
    # PROBLEMA: ¡Está intentando devolver su propio resultado!
    return(datos_saber_muestra) 
  } else {
    # PROBLEMA: ¡Está intentando filtrar su propio resultado!
    return(datos_saber_muestra %>% filter(cole_naturaleza == input$naturaleza_colegio))
  }
})

La causa: El conductor `datos_saber_muestra` se está llamando a sí mismo dentro de su propia definición. Es como decir: "Para definir X, primero necesito el valor de X".

Código Corregido
# El conductor ahora parte del dataframe original "datos_saber"
datos_filtrados <- eventReactive(input$run_analysis, {
  # Usamos el dataframe original como fuente
  df <- datos_saber 
  
  if (input$naturaleza_colegio == "Todos") {
    return(df)
  } else {
    return(df %>% filter(cole_naturaleza == input$naturaleza_colegio))
  }
})

La solución: Un conductor reactivo siempre debe partir de una fuente no reactiva (como el `datos_saber` original) o de *otro* conductor reactivo ya definido.

Error 2: El "Contexto Reactivo" Perdido

Este error ilustra la regla más importante de Shiny: solo puedes usar un valor reactivo (ej. `datos_filtrados()`) dentro de un contexto reactivo (ej. `renderText`, `observeEvent`).

Código Problemático
# 1. Se intenta llamar a un reactive FUERA de un contexto reactivo
total_estudiantes_kpi <- format(nrow(datos_filtrados())) 

# 2. El renderText tiene una sintaxis incorrecta
output$total_estudiantes_kpi <- renderText({
  total_estudiantes_kpi,  # Esto no funciona como se espera
  big.mark = "," 
})

La causa: El código fuera de un `render...` o `observe...` se ejecuta solo una vez al inicio. No puedes llamar a `datos_filtrados()` ahí porque su valor depende de interacciones del usuario que aún no han ocurrido.

Código Corregido
# La lógica completa se mueve DENTRO del renderText
output$total_estudiantes_kpi <- renderText({
  
  # 1. Llama al reactive para obtener el dataframe filtrado
  df_filtrado <- datos_filtrados()
  
  # 2. Calcula y formatea el resultado en un solo paso
  total <- nrow(df_filtrado)
  
  # 3. La función devuelve el valor final formateado
  format(total, big.mark = ",")
})

La solución: Toda la lógica que dependa de un valor reactivo debe estar contenida **dentro** de un bloque de código reactivo. Así, Shiny sabe que debe re-ejecutar ese bloque cuando el valor reactivo del que depende cambie.