Este é o módulo de virada: onde o app deixa de ser um script e vira software. A ferramenta central são os módulos Shiny — a forma de dividir um app grande em componentes reutilizáveis e isolados, sem colisão de IDs. Somamos a isso o controle do navegador (shinyjs), a comunicação customizada entre JavaScript e R, e uma noção de como estender o Shiny com seus próprios widgets. É a base técnica para o Golem.
Dominar módulos Shiny (NS, moduleServer, comunicação entre módulos), manipular a UI e o navegador com shinyjs, trocar mensagens customizadas entre JavaScript e R (Shiny.setInputValue, custom message handlers), e entender processamento assíncrono com promises/future.
O problema que os módulos resolvem
Imagine dois gráficos idênticos com seus próprios controles. Sem módulos, você duplica os IDs:
# SEM módulos — IDs colidem, código duplicado
ui <- fluidPage(
selectInput("var1", ...), plotOutput("plot1"),
selectInput("var2", ...), plotOutput("plot2") # copiar-colar com sufixos
)
Com dez componentes, isso vira um pesadelo de var1, var2, ..., var10, e um server gigante. Módulos Shiny resolvem isso encapsulando UI + server de um componente num par de funções reutilizáveis, com namespace próprio — os IDs não colidem porque cada instância vive no seu espaço.
Anatomia de um módulo
Um módulo é um par: uma função de UI e uma função de server. A chave é o namespace (NS), que prefixa os IDs de cada instância.
library(shiny)
# --- UI do módulo: recebe um 'id' e cria um namespace ---
histogramaUI <- function(id) {
ns <- NS(id) # cria a função de namespace
tagList(
selectInput(ns("var"), "Variável", names(mtcars)), # ns() prefixa o ID
sliderInput(ns("bins"), "Bins", 5, 50, 20),
plotOutput(ns("plot"))
)
}
# --- Server do módulo: usa moduleServer ---
histogramaServer <- function(id) {
moduleServer(id, function(input, output, session) {
output$plot <- renderPlot({
hist(mtcars[[input$var]], breaks = input$bins,
col = "steelblue", main = input$var)
})
})
}
# --- App que usa DUAS instâncias do mesmo módulo, sem colisão ---
ui <- fluidPage(
fluidRow(
column(6, histogramaUI("h1")), # instância "h1"
column(6, histogramaUI("h2")) # instância "h2"
)
)
server <- function(input, output, session) {
histogramaServer("h1") # liga o server da instância h1
histogramaServer("h2") # liga o server da instância h2
}
shinyApp(ui, server)
O que torna isso possível: NS("h1")("var") produz o ID "h1-var", e NS("h2")("var") produz "h2-var" — IDs distintos automaticamente. Dentro do módulo, você escreve só input$var; o namespace é aplicado nos bastidores. Um módulo é uma "caixa" isolada que você instancia quantas vezes quiser.
Comunicação entre módulos
Módulos precisam conversar: um filtro alimenta um gráfico, uma seleção de tabela alimenta um detalhe. A regra de ouro: passe reativos como argumentos (entrada) e retorne reativos (saída). Nunca acesse o input de um módulo de fora.
# Módulo FILTRO: retorna um reactive com os dados filtrados (SAÍDA)
filtroServer <- function(id, dados) {
moduleServer(id, function(input, output, session) {
reactive({
dados |> dplyr::filter(regiao == input$regiao)
}) # o módulo RETORNA um reactive
})
}
# Módulo GRÁFICO: recebe um reactive de dados (ENTRADA)
graficoServer <- function(id, dados_filtrados) {
moduleServer(id, function(input, output, session) {
output$plot <- renderPlot({
plot(dados_filtrados()) # usa o reactive recebido
})
})
}
# App conecta os módulos: saída de um vira entrada do outro
server <- function(input, output, session) {
dados_filt <- filtroServer("filtro", vendas) # captura o reactive retornado
graficoServer("grafico", dados_filt) # injeta como entrada
}
Esse contrato — entrada por argumento, saída por retorno — mantém os módulos desacoplados e testáveis. É a mesma disciplina de funções puras, aplicada à reatividade.
shinyjs: controlar o navegador a partir do R
O pacote shinyjs dá verbos R para manipular a página sem escrever JavaScript: mostrar/esconder, habilitar/desabilitar, adicionar classe CSS, rodar JS arbitrário.
library(shinyjs)
ui <- fluidPage(
useShinyjs(), # OBRIGATÓRIO ativar
actionButton("toggle", "Mostrar/Esconder"),
actionButton("processar", "Processar"),
div(id = "painel", "Conteúdo do painel", plotOutput("g"))
)
server <- function(input, output, session) {
observeEvent(input$toggle, toggle("painel")) # mostra/esconde
observeEvent(input$processar, {
disable("processar") # evita clique duplo
shinyjs::html("processar", "Processando...")
resultado <- tarefa()
enable("processar")
shinyjs::html("processar", "Processar")
})
}
Verbos mais úteis: show/hide/toggle, enable/disable (desabilitar botão durante processamento evita cliques duplos), addClass/removeClass/toggleClass (combina com o CSS de identidade visual), runjs("código") (JS arbitrário) e delay(ms, expr).
A ponte JS ↔ R nos dois sentidos
Em visualização geoespacial e redes um widget mandou dados para o R via Shiny.setInputValue. Existe o caminho inverso: o R mandar mensagens para o JavaScript, via custom message handlers.
# --- No JS (www/handlers.js): registra um handler nomeado ---
# Shiny.addCustomMessageHandler('mostrar_alerta', function(msg) {
# alert(msg.texto); // recebe dados do R e age no navegador
# });
# --- No R: envia mensagem para aquele handler ---
observeEvent(input$avisar, {
session$sendCustomMessage(
"mostrar_alerta", # nome do handler JS
list(texto = "Olá do servidor R!") # dados (viram objeto JS)
)
})
Os dois sentidos, resumidos:
- JS → R:
Shiny.setInputValue("nome", valor)no JS ⟶input$nomeno R. - R → JS:
session$sendCustomMessage("handler", dados)no R ⟶Shiny.addCustomMessageHandler("handler", fn)no JS.
Com esses dois, você integra qualquer biblioteca JavaScript ao Shiny — um mapa exótico, um editor de texto rico, uma animação — sem esperar que exista um pacote R pronto.
Estendendo o Shiny: uma noção de htmlwidgets
Quando você quer empacotar uma biblioteca JS como um widget R reutilizável (do jeito que plotly, leaflet e DT são), o framework é htmlwidgets. Ele padroniza a ponte: você define como os dados R viram JSON, e um arquivo JS que os recebe e desenha.
# Esqueleto conceitual (htmlwidgets::createWidget)
meuWidget <- function(dados, width = NULL, height = NULL) {
htmlwidgets::createWidget(
name = "meuWidget",
x = list(dados = dados), # vira JSON, entregue ao JS
width = width, height = height,
package = "meupacote"
)
}
# O arquivo JS correspondente implementa renderValue(el, x) para desenhar.
Você não precisa escrever htmlwidgets no dia a dia, mas entender que plotly/leaflet/visNetwork são htmlwidgets explica por que todos têm *Output/render* e proxies parecidos — é o mesmo padrão por baixo.
Assíncrono: apps que não travam com promises + future
Por padrão, o Shiny é single-threaded por sessão: uma tarefa longa de um usuário congela o app inteiro para ele (e, em cenários simples, atrapalha outros). A solução é rodar o trabalho pesado em outro processo com future e integrá-lo à reatividade com promises.
library(promises)
library(future)
plan(multisession) # usa vários processos
server <- function(input, output, session) {
output$resultado <- renderText({
input$rodar
future_promise({ # roda em processo separado
Sys.sleep(5) # tarefa longa NÃO trava a UI
calcular_pesado()
}) %...>% (function(res) { # %...>% encadeia o resultado
paste("Pronto:", res)
})
})
}
O operador %...>% ("promise pipe") diz "quando a promessa resolver, faça isto com o resultado". Enquanto o future roda em outro processo, a interface continua responsiva. É o que permite um app servir vários usuários sem que uma tarefa pesada de um bloqueie os demais — assunto que reaparece na escalabilidade de ShinyProxy.
Os módulos Shiny encapsulam UI e server de um componente com namespace próprio, via NS e moduleServer, o que elimina colisão de IDs e torna o componente reutilizável. O NS(id)("x") gera IDs prefixados como id-x, e dentro do módulo se escreve apenas input$x. A comunicação entre módulos segue uma regra fechada: entrada por argumento, saída por retorno, ambas reativas — nunca acessar o input de outro módulo diretamente. O shinyjs controla o navegador a partir do R, com show, hide, toggle, enable e disable (o antídoto do clique duplo), addClass, runjs e delay. A ponte entre as duas linguagens tem os dois sentidos: de JS para R com Shiny.setInputValue chegando em input$, e de R para JS com session$sendCustomMessage recebido por addCustomMessageHandler — é assim que qualquer biblioteca JS se integra. Por baixo de plotly, leaflet e DT está o htmlwidgets, e é ele que explica o padrão comum *Output, render* e proxy. Para tarefas longas, o assíncrono com promises e future — plan(multisession), future_promise, %...>% — impede que elas travem a interface.
Exercícios
-
Qual problema concreto os módulos Shiny resolvem, e como o namespace (
NS) o resolve?✓ Resposta:Resolvem a colisão de IDs e a duplicação de código quando um app tem componentes repetidos ou cresce muito. Sem módulos, cada
inputId/outputIdé global e único, forçando sufixos manuais (var1,var2...) e umservermonolítico. ONS(id)cria um namespace:NS("h1")("var")vira"h1-var"eNS("h2")("var")vira"h2-var", de modo que cada instância do módulo tem IDs distintos automaticamente. Assim o mesmo módulo pode ser instanciado várias vezes sem conflito, e cada componente fica isolado e reutilizável. -
Um módulo de filtro precisa alimentar um módulo de gráfico. Como se faz a comunicação corretamente?
✓ Resposta:Pelo contrato entrada por argumento, saída por retorno. O módulo de filtro retorna um
reactivecom os dados filtrados:dados_filt <- filtroServer("filtro", dados). O módulo de gráfico recebe esse reactive como argumento:graficoServer("grafico", dados_filt), e o usa internamente comdados_filtrados(). Nunca se acessainputde um módulo a partir de outro — a comunicação passa sempre por reativos explícitos, o que mantém os módulos desacoplados e testáveis. -
Por que desabilitar um botão com
shinyjs::disabledurante o processamento é uma boa prática?✓ Resposta:Para evitar cliques duplos/múltiplos enquanto a operação anterior ainda está em andamento. Sem desabilitar, um usuário impaciente pode clicar várias vezes em "processar"/"salvar", disparando a ação repetidas vezes — duplicando registros, refazendo cálculos caros ou corrompendo estado.
disable("btn")no início eenable("btn")ao final (idealmente comon.exitpara garantir a reabilitação mesmo em erro) tornam a ação idempotente do ponto de vista do usuário e melhoram o feedback. -
Descreva os dois sentidos da comunicação JS↔R no Shiny e quando usar o sentido R→JS.
✓ Resposta:JS→R: no navegador,
Shiny.setInputValue("nome", valor)cria/atualizainput$nomeno servidor — usado para levar interações de widgets/JS até a reatividade (ex.: clique num nó de rede). R→JS: no servidor,session$sendCustomMessage("handler", dados)envia dados a umShiny.addCustomMessageHandler("handler", fn)registrado no JS — usado quando o R precisa mandar o navegador fazer algo que não é um output padrão: disparar um alerta, animar um elemento, chamar um método de uma biblioteca JS externa, acionar impressão. Juntos, permitem integrar qualquer biblioteca JavaScript sem depender de um pacote R pronto. -
Seu app trava para o usuário durante um cálculo de 10 segundos. Qual a abordagem para manter a UI responsiva e como ela funciona em linhas gerais?
✓ Resposta:Rodar o cálculo pesado fora do processo principal com
futuree integrá-lo à reatividade compromises. Define-seplan(multisession)para habilitar processos paralelos; envolve-se o trabalho emfuture_promise({ ... }), que executa num processo separado; e encadeia-se o resultado com o promise pipe%...>%. Enquanto ofuturecalcula, o processo principal do Shiny fica livre para responder à interface — a UI não congela e outros usuários não ficam bloqueados por essa tarefa. É a base para escalar apps com operações demoradas.