Quase todo bug difícil em Shiny é, no fundo, um mal-entendido sobre reatividade. O código "roda quando eu não queria", "não roda quando eu queria", ou "roda vezes demais". Antes de sincronizar gráficos, servir mapas e modularizar apps, precisamos de um modelo mental correto de quando e por que o Shiny reexecuta um pedaço de código. Esta aula é o alicerce; volte a ela sempre que algo reativo se comportar de forma inesperada.

Construir um modelo mental preciso do grafo de reatividade do Shiny: as três famílias de blocos reativos (fontes, condutores e sumidouros), a avaliação preguiçosa, reactive() vs observe() vs eventReactive(), isolate(), req()/validate(), e o gerenciamento de estado com reactiveVal()/reactiveValues().

O modelo: fontes, condutores e sumidouros

O Shiny mantém um grafo de dependências. Cada nó é um pedaço de código reativo, e cada aresta significa "eu leio o valor daquele nó, então me reexecute quando ele mudar". Existem três papéis:

  • Fontes reativas (reactive sources): os input$*. Mudam quando o usuário interage. Só podem ser lidos, nunca escritos diretamente.
  • Condutores reativos (reactive conductors): expressões reactive({...}). Ficam no meio — dependem de fontes/outros condutores e são lidos por outros nós. São preguiçosos (só calculam se alguém pedir) e cacheados (não recalculam se as dependências não mudaram).
  • Sumidouros reativos (reactive endpoints): os output$* (via render*) e os observe()/observeEvent(). São o "fim da linha" — produzem efeitos colaterais (desenham um gráfico, gravam no banco). São ansiosos (executam sozinhos quando invalidados).
library(shiny)

ui <- fluidPage(
  sliderInput("n", "Amostra:", min = 10, max = 1000, value = 100),
  verbatimTextOutput("resumo"),
  plotOutput("hist")
)

server <- function(input, output, session) {

  # CONDUTOR: calcula uma vez por mudança de input$n, cacheia,
  # e é reusado por dois sumidouros (resumo e hist).
  amostra <- reactive({
    rnorm(input$n)          # depende de input$n
  })

  # SUMIDOURO 1
  output$resumo <- renderPrint({
    summary(amostra())      # lê o condutor com ()
  })

  # SUMIDOURO 2
  output$hist <- renderPlot({
    hist(amostra(), col = "steelblue", main = NULL)
  })
}

shinyApp(ui, server)

A lição central: amostra é calculado uma vez por mudança de input$n, mesmo sendo lido por dois outputs. Se você tivesse colocado rnorm(input$n) diretamente dentro de cada render*, teria gerado duas amostras diferentes e feito o trabalho duas vezes. Condutores existem para compartilhar e cachear trabalho.

Preguiça e invalidação — o que dispara o quê

Duas regras governam tudo:

  1. Sumidouros são ansiosos: um output$* visível na tela e todo observe() executam assim que suas dependências mudam.
  2. Condutores são preguiçosos: um reactive() só executa se um sumidouro (direta ou indiretamente) o ler. Se nada o lê, ele nunca roda.
server <- function(input, output, session) {
  caro <- reactive({
    message("Calculando algo caro...")   # aparece no console
    Sys.sleep(1)
    input$n * 2
  })

  # Se este output NÃO existir na UI, 'caro()' nunca é calculado.
  output$valor <- renderText({
    paste("Resultado:", caro())
  })
}

Quando input$n muda, o Shiny invalida caro e output$valor. Na próxima "descarga" (flush) do ciclo reativo, ele recalcula apenas o que é necessário e está sendo observado. Esse comportamento preguiçoso é o que impede um app grande de recalcular o mundo inteiro a cada clique.

reactive() vs observe() vs observeEvent()

Esta é a distinção que mais confunde. Regra de ouro:

  • reactive() — CALCULA E RETORNA um valor. Use quando você quer produzir dados para outros nós lerem. Não deve ter efeito colateral.
  • observe() — FAZ algo (efeito colateral), não retorna nada útil. Use para atualizar inputs, escrever em reactiveValues, logar. Reage a todas as reativas que lê.
  • observeEvent() — FAZ algo em resposta a UM gatilho específico. Ignora mudanças em tudo o mais.
server <- function(input, output, session) {
  rv <- reactiveValues(cliques = 0)

  # CONDUTOR: retorna valor, sem efeito colateral
  dobro <- reactive(input$n * 2)

  # OBSERVE: efeito colateral (atualiza outro input) sempre que 'dobro' muda
  observe({
    updateSliderInput(session, "n2", value = dobro())
  })

  # OBSERVE_EVENT: só dispara no clique do botão; ignora input$n
  observeEvent(input$salvar, {
    rv$cliques <- rv$cliques + 1
    showNotification(paste("Salvo", rv$cliques, "vezes"))
  })
}

A dupla equivalente para valores é eventReactive(): um reactive que só recalcula quando um gatilho específico dispara — perfeito para o padrão "só recalcule quando eu clicar em Rodar".

resultado <- eventReactive(input$rodar, {
  modelo_pesado(input$param)   # só roda ao clicar 'rodar', não a cada 'param'
})

output$saida <- renderPlot(plot(resultado()))

Compare mentalmente: observeEvent(gatilho, {efeito}) faz algo; eventReactive(gatilho, {valor}) produz algo. Mesma ideia de "só no gatilho", papéis diferentes.

isolate() — ler sem criar dependência

Às vezes você quer usar o valor de uma reativa sem que sua mudança dispare a reexecução. isolate() lê o valor "congelado", sem criar uma aresta no grafo.

observeEvent(input$rodar, {
  # Dispara SÓ no clique de 'rodar'.
  # Lê input$texto e input$n, mas mudá-los NÃO redispara isto:
  registrar(isolate(input$texto), isolate(input$n))
})

Na prática, eventReactive/observeEvent já isolam tudo que está no corpo — o gatilho é o único disparo. Você recorre a isolate() manual em casos mais finos, dentro de um observe/reactive comum, quando quer que algumas leituras não criem dependência.

req() e validate() — parar de propósito

Nem toda reexecução deve prosseguir. Se um input ainda está vazio (NULL), ou um upload não chegou, você quer abortar silenciosamente em vez de estourar um erro. É o papel de req().

output$tabela <- renderTable({
  req(input$arquivo)                 # se NULL/"" , para aqui sem erro
  dados <- read.csv(input$arquivo$datapath)
  req(nrow(dados) > 0, cancelOutput = TRUE)  # mantém saída anterior
  head(dados)
})

Quando você quer mostrar uma mensagem ao usuário em vez de parar em silêncio, use validate(need(...)):

output$plot <- renderPlot({
  validate(
    need(input$coluna != "", "Selecione uma coluna para plotar."),
    need(is.numeric(dados()[[input$coluna]]), "A coluna precisa ser numérica.")
  )
  hist(dados()[[input$coluna]])
})

A diferença: req() aborta silenciosamente (bom para pré-condições técnicas); validate(need()) exibe um texto amigável no lugar do output (bom para orientar o usuário). Isso é aprofundado em notificações, validação e segurança.

Estado: reactiveVal() e reactiveValues()

Fontes (input$*) são controladas pelo usuário. Mas às vezes o app precisa manter estado próprio: um contador, uma lista de itens selecionados, o resultado de uma ação. Para isso existem contêineres reativos que você mesmo pode escrever.

server <- function(input, output, session) {

  # reactiveVal: um único valor mutável
  contador <- reactiveVal(0)

  observeEvent(input$mais, {
    contador(contador() + 1)   # ler: contador()  | escrever: contador(x)
  })

  # reactiveValues: vários campos, como uma lista reativa
  estado <- reactiveValues(carrinho = character(0), total = 0)

  observeEvent(input$add, {
    estado$carrinho <- c(estado$carrinho, input$produto)
    estado$total    <- estado$total + input$preco
  })

  output$saldo <- renderText(paste("Itens:", length(estado$carrinho)))
}

Regra: input$* é estado do usuário; reactiveVal/reactiveValues é estado do app. Dominar essa distinção é o que separa apps que "esquecem" o que o usuário fez de apps que mantêm uma sessão coerente. Esse padrão de "estado central em reactiveValues" é a espinha dorsal de apps modulares (programação em Shiny).

Um erro clássico: efeito colateral dentro de reactive()

# ERRADO: reactive com efeito colateral
dados <- reactive({
  x <- read.csv(input$arq$datapath)
  write.csv(x, "log.csv")   # efeito colateral escondido!
  x
})

Como reactive é preguiçoso e cacheado, o write.csv roda de forma imprevisível — só quando alguém lê dados(), e não roda de novo se estiver cacheado. Efeitos colaterais pertencem a observe/observeEvent. Mantenha reactive() puro: entra dependência, sai valor, sem surpresas.

O Shiny mantém um grafo reativo com três famílias: fontes, que são os input; condutores, os reactive; e sumidouros, os output e observe. A distinção que organiza o resto é o temperamento de cada um — condutores são preguiçosos e cacheados, só calculam quando alguém pede e guardam o resultado; sumidouros são ansiosos e rodam por conta própria. Daí a regra prática: use reactive() para compartilhar trabalho entre vários pontos. Os três verbos se dividem com clareza: reactive() retorna valor e deve ser puro; observe() e observeEvent() fazem efeito; eventReactive() retorna valor, mas só quando o gatilho dispara. O isolate() lê sem criar dependência, e tanto observeEvent quanto eventReactive já isolam o próprio corpo. Para interromper de propósito há dois caminhos: req() aborta em silêncio, validate(need()) mostra mensagem ao usuário. O estado do app mora em reactiveVal e reactiveValues, onde você pode escrever; o input$* é estado do usuário e só se lê. E a regra que evita a maior parte dos bugs difíceis: nunca coloque efeito colateral dentro de reactive().

Exercícios

  1. Você tem um cálculo caro usado por três outputs. Colocá-lo diretamente em cada render* ou em um reactive() compartilhado? Por quê?

    ✓ Resposta:

    Em um reactive() compartilhado. O condutor calcula uma vez por mudança de dependência e cacheia o resultado; os três outputs leem o mesmo valor. Repetir o cálculo em cada render* faria o trabalho três vezes e, se houver aleatoriedade (rnorm, amostragem), produziria resultados diferentes em cada output — uma inconsistência sutil e difícil de rastrear.

  2. Qual a diferença prática entre observeEvent(input$go, {...}) e eventReactive(input$go, {...})?

    ✓ Resposta:

    Ambos só disparam quando input$go muda. observeEvent executa um efeito colateral e não retorna valor utilizável (ex.: salvar no banco, mostrar notificação). eventReactive retorna um valor que outros nós reativos podem ler com () (ex.: o resultado de um modelo, para alimentar um gráfico). Escolha pelo que você precisa na saída: fazer algo → observeEvent; produzir um valor → eventReactive.

  3. O código abaixo mostra um erro vermelho quando o app abre, antes de o usuário selecionar um arquivo. Corrija-o. > ``r > output$tab <- renderTable({ > d <- read.csv(input$arquivo$datapath) > head(d) > }) > ``

    ✓ Resposta:

    Antes de qualquer upload, input$arquivo é NULL, e read.csv(NULL$datapath) estoura. Basta adicionar uma pré-condição:

    output$tab <- renderTable({
      req(input$arquivo)     # aborta silenciosamente até haver upload
      d <- read.csv(input$arquivo$datapath)
      head(d)
    })

    req() interrompe a execução sem erro enquanto a pré-condição não é satisfeita, deixando o output vazio e limpo.

  4. Por que colocar write.csv(x, "log.csv") dentro de um reactive() é uma má ideia?

    ✓ Resposta:

    Porque reactive() é preguiçoso e cacheado: o corpo só roda quando alguém lê o valor, e não roda de novo enquanto as dependências não mudam. O write.csv — um efeito colateral — passa a acontecer de forma imprevisível (às vezes não acontece, às vezes acontece tarde). Efeitos colaterais devem ficar em observe()/observeEvent(), que são ansiosos e existem justamente para isso. reactive() deve ser puro: dependências entram, valor sai.

  5. Você quer um contador que aumenta a cada clique de um botão e persiste durante a sessão. Escreva o server.

    ✓ Resposta:
    server <- function(input, output, session) {
      n <- reactiveVal(0)
      observeEvent(input$clique, {
        n(n() + 1)
      })
      output$total <- renderText(paste("Cliques:", n()))
    }

    reactiveVal(0) cria um valor mutável inicializado em 0; n() lê, n(x) escreve. O observeEvent incrementa a cada clique, e o output reflete o estado atual. Como o estado é do app (não do usuário), input$* não serviria — daí reactiveVal.

Fontes e leituras recomendadas