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$nome no 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

  1. 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 um server monolítico. O NS(id) cria um namespace: NS("h1")("var") vira "h1-var" e NS("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.

  2. 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 reactive com 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 com dados_filtrados(). Nunca se acessa input de 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.

  3. Por que desabilitar um botão com shinyjs::disable durante 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 e enable("btn") ao final (idealmente com on.exit para garantir a reabilitação mesmo em erro) tornam a ação idempotente do ponto de vista do usuário e melhoram o feedback.

  4. 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/atualiza input$nome no 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 um Shiny.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.

  5. 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 future e integrá-lo à reatividade com promises. Define-se plan(multisession) para habilitar processos paralelos; envolve-se o trabalho em future_promise({ ... }), que executa num processo separado; e encadeia-se o resultado com o promise pipe %...>%. Enquanto o future calcula, 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.

Fontes e leituras recomendadas