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$*(viarender*) e osobserve()/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:
- Sumidouros são ansiosos: um
output$*visível na tela e todoobserve()executam assim que suas dependências mudam. - 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 emreactiveValues, 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
-
Você tem um cálculo caro usado por três outputs. Colocá-lo diretamente em cada
render*ou em umreactive()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 cadarender*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. -
Qual a diferença prática entre
observeEvent(input$go, {...})eeventReactive(input$go, {...})?✓ Resposta:Ambos só disparam quando
input$gomuda.observeEventexecuta um efeito colateral e não retorna valor utilizável (ex.: salvar no banco, mostrar notificação).eventReactiveretorna 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. -
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, eread.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. -
Por que colocar
write.csv(x, "log.csv")dentro de umreactive()é 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. Owrite.csv— um efeito colateral — passa a acontecer de forma imprevisível (às vezes não acontece, às vezes acontece tarde). Efeitos colaterais devem ficar emobserve()/observeEvent(), que são ansiosos e existem justamente para isso.reactive()deve ser puro: dependências entram, valor sai. -
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. OobserveEventincrementa a cada clique, e ooutputreflete o estado atual. Como o estado é do app (não do usuário),input$*não serviria — daíreactiveVal.