Modules Shiny : composants UI + serveur réutilisables
Écrivez un composant Shiny une fois, réutilisez-le de nombreuses fois sans collision d’ids, et prouvez que sa logique fonctionne avec testServer() — sans navigateur requis.
Les modules Shiny rendent une paire UI+serveur réutilisable et isolée. NS(id) munit chaque id d’entrée/sortie d’un espace de noms pour que plusieurs instances n’entrent jamais en collision, moduleServer() contient la logique serveur, les modules communiquent en renvoyant et en passant des réactifs, et testServer() pilote un module sans navigateur pour prouver qu’il fonctionne. La primitive de réutilisation sur laquelle golem s’appuie avec add_module().
- Un module est une paire UI + serveur réutilisable dotée de son propre espace de noms. Vous écrivez le composant une seule fois et vous le déposez dans votre application autant de fois que nécessaire, chaque instance isolée des autres.
NS(id)est toute l’astuce. Dans l’UI du module, vous enveloppez chaque id d’entrée/sortie dansns <- NS(id), si bien quens("add")dans un module utilisé avec l’id"counter"devient l’identifiant globalement unique"counter-add"— deux instances ne peuvent pas entrer en collision.moduleServer()contient la logique, et à l’intérieur vous utilisez l’id simple (input$add, et noninput$`counter-add`) — le module voit des ids non préfixés.- Les modules communiquent par des réactifs, non par des variables globales partagées. Un serveur de module renvoie un réactif que le parent lit ; vous passez des réactifs à un module comme arguments de fonction. Les modules n’accèdent jamais à l’espace de noms les uns des autres.
- The runtime is the judge.
testServer()pilote la fonction serveur d’un module sans navigateur — vous fixez les entrées, lisez le réactif, vérifiez le résultat — prouvant que la logique fonctionne sans lancer de navigateur.
Introduction
Votre application Shiny comporte deux onglets presque identiques : chacun a un curseur, un tableau et un graphique qui filtre selon une plage choisie. Vous avez construit le second en copiant-collant le premier et en renommant chaque input$ et output$ — range1 → range2, table1 → table2, et ainsi de suite. Ça marche. Puis un troisième onglet arrive, et vous êtes sur le point de coller une troisième copie. Chaque id dupliqué est un bug qui ne demande qu’à survenir : oubliez d’en renommer un, et deux contrôles partagent silencieusement le même fil.
Les modules Shiny résolvent ce problème. Un module est une paire UI + serveur autonome que vous écrivez une seule fois et réutilisez avec un id unique à chaque apparition — et chaque instance est isolée par espace de noms, si bien que les ids n’entrent jamais en conflit, quel que soit le nombre de copies que vous déposez. Au lieu de trois copies de l’onglet de filtre renommées à la main, vous écrivez une seule paire filterUI() / filterServer() et l’appelez trois fois.
Les modules sont la primitive de réutilisation sur laquelle repose toute la montée en charge de Shiny — c’est exactement ce que génère pour vous la fonction add_module() de golem, et ce qu’enseignent l’article officiel sur les modules Shiny et le chapitre sur les modules de Mastering Shiny comme le chemin d’un script vers une application.
Quand ne pas recourir à un module. Un bloc ponctuel qui n’apparaît qu’une seule fois et ne sera pas réutilisé n’a pas besoin de cet emballage — un module ici est une surcharge, pas de l’organisation. Les modules valent leur coût lorsqu’un composant est répété, assez complexe pour être isolé ou partagé entre applications. Si c’est un petit morceau d’UI unique utilisé une fois, laissez-le en ligne.
Les modules s’appuient directement sur la programmation réactive — reactiveVal(), observeEvent(), reactive(), et la façon dont les sorties se réexécutent quand leurs entrées changent. Si tout cela n’est pas encore un réflexe, lisez d’abord la leçon sur la programmation réactive de cette série ; tout ce qui suit suppose que vous êtes à l’aise avec les sources et les points de sortie réactifs. Il vous suffit d’avoir Shiny installé :
install.packages("shiny")Les applications ci-dessous lancent un serveur en direct : rien sur cette page n’est exécuté ici — une page web statique ne peut pas exécuter une application Shiny. Copiez n’importe quel bloc sur votre machine et exécutez-le. Le meilleur, c’est que la logique d’un module n’a même pas besoin d’un navigateur pour être vérifiée : testServer() (montré à la fin) pilote la fonction serveur d’un module sans navigateur et vous permet de vérifier ce qu’elle calcule. Copiez le code du module et exécutez-le pour reproduire chaque résultat de cette page. The runtime is the judge.
Le problème de l’espace de noms
Shiny relie les entrées aux sorties par leur id. input$add sur le serveur désigne l’élément d’UI portant l’id "add" — l’id est un nom global. Donc si vous collez deux fois la même UI, vous obtenez deux éléments tous deux nommés "add", et Shiny ne peut pas les distinguer. Voilà la collision.
La solution est un espace de noms (NS) : un préfixe qui rend uniques les ids de chaque instance. Shiny vous fournit NS(id), une fabrique de fonctions. Appelez NS("counter") et vous obtenez une fonction qui préfixe n’importe quel id par "counter-" :
ns <- NS("counter")
ns("add") # "counter-add"
ns("n") # "counter-n"Ainsi, un module utilisé avec l’id "counter" a un bouton interne "counter-add", tandis que le même module utilisé avec l’id "total" a "total-add". Deux instances, zéro collision — le préfixe d’id les maintient séparées. C’est autour de cette seule idée qu’un module est construit : enveloppez chaque id de l’UI avec ns(), et vous pouvez réutiliser le composant librement.
Construire un module
Un module, ce sont deux fonctions. La fonction UI prend un id, construit ns <- NS(id) et enveloppe chaque id d’entrée/sortie dans ns(). La fonction serveur prend le même id et le passe à moduleServer(), qui exécute votre logique dans cet espace de noms.
Voici un module complet et minimal — un compteur de clics. Lisez les deux moitiés, puis le câblage ci-dessous :
library(shiny)
# UI half — every id is wrapped in ns()
counterUI <- function(id) {
ns <- NS(id)
tagList(
actionButton(ns("add"), "Add"),
textOutput(ns("n"))
)
}
# Server half — the logic, run inside the module's namespace
counterServer <- function(id) {
moduleServer(id, function(input, output, session) {
count <- reactiveVal(0)
observeEvent(input$add, count(count() + 1))
output$n <- renderText(count())
count # return a reactive so the parent can read it
})
}Deux points méritent qu’on s’y arrête. D’abord, dans counterUI(), les ids passés à actionButton() et textOutput() transitent par ns() — c’est ce qui les munit d’un espace de noms. tagList() ne fait que regrouper les éléments d’UI en un seul objet renvoyable (une UI de module doit renvoyer un unique objet tag, et tagList() est le regroupeur standard).
Ensuite — et c’est ce qui piège tout le monde une fois — à l’intérieur de moduleServer(), vous utilisez l’id simple : input$add, et non input$`counter-add`. Le serveur du module opère au sein de son propre espace de noms, il voit donc des ids non préfixés. Le préfixage par NS() se fait du côté de l’UI et dans la plomberie ; votre logique serveur lit input$add et écrit output$n exactement comme s’il s’agissait d’une petite application autonome. C’est là tout l’intérêt de la conception en modules : la logique se lit normalement, et l’espace de noms est invisible de l’intérieur.
La dernière ligne, count, renvoie le réactif — c’est ainsi qu’un module restitue une valeur à ce qui l’a utilisé. Nous nous en servirons dans un instant.
Réutilisez-le, deux fois, sans collision
Voici maintenant la raison d’être des modules. Déposez le compteur dans une application deux fois, avec deux ids différents — "a" et "b" — et il n’y a aucune collision à gérer, aucun id à renommer :
library(shiny)
ui <- fluidPage(
counterUI("a"),
counterUI("b")
)
server <- function(input, output, session) {
counterServer("a")
counterServer("b")
}
shinyApp(ui, server)En coulisses, l’instance "a" possède les ids "a-add" / "a-n" et l’instance "b" possède "b-add" / "b-n" — des fils entièrement séparés. Cliquez sur le premier « Add » et seul le premier compteur bouge. Comparez cela à l’approche copier-coller : ici, vous avez ajouté un second compteur avec une ligne supplémentaire par moitié et n’avez rien renommé. Vous en voulez un troisième ? Ajoutez counterUI("c") et counterServer("c"). Voilà le gain de mise à l’échelle en un écran de code.
Les modules communiquent par des réactifs
Les vraies applications ont des modules qui dépendent les uns des autres : un filtre alimente un graphique, un sélecteur pilote un tableau. Pour cela, les modules n’accèdent pas à l’espace de noms les uns des autres — cela ruinerait l’isolation. À la place, ils se passent des réactifs :
- Un serveur de module renvoie un réactif (comme
countci-dessus) ; le parent le capture —selected <- filterServer("f")— et le lit avecselected(). - Vous passez un réactif à un module comme simple argument de fonction —
plotServer("p", data = selected)— et le module le lit à l’intérieur avecdata().
Voilà tout le modèle de communication : les valeurs remontent vers le haut sous forme de réactifs renvoyés et descendent vers le bas sous forme de réactifs passés en argument. Voici une petite paire filtre → résumé sur les données intégrées mtcars. Le module de filtre renvoie les données filtrées sous forme de réactif ; le module de résumé reçoit ce réactif et rapporte le nombre de lignes :
library(shiny)
# A filter module: returns the filtered data as a reactive
filterUI <- function(id) {
ns <- NS(id)
tagList(
sliderInput(ns("mpg"), "Minimum mpg", min = 10, max = 35, value = 20)
)
}
filterServer <- function(id) {
moduleServer(id, function(input, output, session) {
reactive(mtcars[mtcars$mpg >= input$mpg, ]) # RETURN the filtered data
})
}
# A summary module: TAKES a data reactive in, reports on it
summaryUI <- function(id) {
ns <- NS(id)
textOutput(ns("count"))
}
summaryServer <- function(id, data) { # data is a reactive arg
moduleServer(id, function(input, output, session) {
output$count <- renderText(paste(nrow(data()), "cars match"))
})
}
ui <- fluidPage(filterUI("f"), summaryUI("s"))
server <- function(input, output, session) {
selected <- filterServer("f") # capture the returned reactive
summaryServer("s", data = selected) # pass it down into the summary module
}
shinyApp(ui, server)Déplacez le curseur et le résumé se met à jour automatiquement — parce que data() à l’intérieur de summaryServer est le réactif selected, si bien que Shiny suit la dépendance à travers la frontière du module. Notez le filtrage en R de base, sans remodelage (mtcars[mtcars$mpg >= input$mpg, ]) : les modules n’exigent aucun package spécialisé de manipulation de données, juste du R ordinaire. Ce motif remontée / descente est ce que le chapitre sur les modules de Mastering Shiny appelle l’interface du module — et limiter la communication à cette interface est ce qui garde compréhensible une grande application.
Quand modulariser — et comment golem les génère
Recourez à un module lorsqu’un composant est réutilisé (le même onglet deux fois, un contrôle partagé entre applications), assez complexe pour être isolé (une fonctionnalité autonome avec ses propres entrées, sorties et état) ou qui vaut la peine d’être testé isolément. Ne le faites pas pour un petit morceau d’UI unique qui n’apparaît qu’une fois — l’emballer ajoute du cérémonial sans bénéfice.
Une fois que vous construisez de nombreux modules, vous voudrez les échafauder de façon cohérente. C’est exactement ce qu’automatise le framework golem : golem::add_module("records") écrit une paire mod_records_ui() / mod_records_server() toute prête — la même forme NS() / moduleServer() que vous venez d’apprendre — et vous rappelle où la brancher. Les modules sont l’unité sur laquelle golem fait grandir une application ; comprendre le motif ici est ce qui donne du sens à ce code généré. La question plus large de savoir où vivent les fichiers de module dans un projet relève de l’organisation du code et du développement de packages, traités dans leurs propres leçons de cette série.
Problèmes fréquents
Vous avez oublié ns() sur un id dans l’UI, et la sortie n’apparaît jamais. C’est le bug numéro un des modules. Si vous écrivez textOutput("n") au lieu de textOutput(ns("n")) dans une UI de module, l’id de sortie n’est pas muni d’un espace de noms, si bien que le output$n du serveur (qui, lui, l’est) ne trouve jamais son élément — la sortie reste silencieusement vide. Correctif : chaque id d’entrée et de sortie d’une UI de module doit passer par ns(). Quand une sortie de module est mystérieusement vide, vérifiez d’abord les ids de l’UI.
Vous avez écrit input$`counter-add` à l’intérieur du serveur. À l’intérieur de moduleServer(), vous utilisez l’id simple, non préfixé — input$add. Le préfixage par espace de noms est appliqué automatiquement à la frontière ; utiliser le nom préfixé (input$`counter-add`) ne renvoie à rien et votre observeEvent ne se déclenche jamais. Règle empirique : ns() n’apparaît que dans l’UI du module, jamais dans sa logique serveur.
Un module a besoin des données du parent, mais vous n’avez pas passé le réactif. Si un module doit réagir à des données produites ailleurs, ces données doivent arriver comme argument réactif (summaryServer("s", data = selected)), et à l’intérieur vous les lisez avec data(). Tenter de lire directement une variable du parent — ou passer la valeur actuelle des données au lieu du réactif — rompt la chaîne réactive, si bien que le module ne se met pas à jour quand la source change. Passez le réactif lui-même, et appelez-le à l’intérieur.
Questions fréquentes
Un module Shiny est une paire réutilisable de fonctions — une fonction UI et une fonction serveur — qui emballent un morceau autonome d’une application avec son propre espace de noms (NS), ce qui vous permet de déposer le même composant plusieurs fois dans votre application sans collision d’ids. La fonction UI enveloppe chaque id d’entrée/sortie dans ns <- NS(id) ; la fonction serveur exécute sa logique à l’intérieur de moduleServer(id, ...). Les modules sont la façon standard de faire monter en charge une application Shiny au-delà d’un unique script (article officiel).
Par des réactifs, non par des variables globales partagées. Un serveur de module renvoie un réactif que le parent capture (selected <- filterServer("f")) et lit avec selected() ; et vous passez des réactifs à un module comme arguments de fonction (plotServer("p", data = selected)), lus à l’intérieur avec data(). Les valeurs remontent sous forme de réactifs renvoyés et descendent sous forme de réactifs passés en argument. Les modules n’accèdent jamais à l’espace de noms les uns des autres — les réactifs renvoyés/passés constituent toute l’interface.
Utilisez-en un lorsqu’un composant est réutilisé (la même UI apparaît plus d’une fois, ou entre applications), assez complexe pour être isolé (une fonctionnalité autonome avec ses propres entrées, sorties et état) ou qui vaut la peine d’être testé isolément. Ne le faites pas pour un petit bloc ponctuel qui n’apparaît qu’une fois — un module ici est une surcharge sans bénéfice. Le signal, c’est la répétition ou une complexité autonome, pas la seule taille de l’application.
ns() dans le serveur du module, ou seulement dans l’UI ?
Seulement dans l’UI. À l’intérieur de moduleServer(), vous utilisez l’id simple, non préfixé — input$add, output$n — parce que le serveur du module s’exécute déjà dans son propre espace de noms et que Shiny gère le préfixage à la frontière. NS() / ns() apparaît dans la fonction UI du module, où vous enveloppez chaque id. (Si jamais vous avez besoin de l’espace de noms à l’intérieur du serveur — par exemple pour construire un id d’un élément d’UI inséré dynamiquement — il est disponible via session$ns, mais pour les entrées et sorties ordinaires, vous n’y touchez pas.)
NS() et moduleServer() ?
Ce sont les deux moitiés d’un module. NS(id) est une fabrique de fonctions utilisée dans l’UI : NS("counter") renvoie une fonction qui préfixe les ids, si bien que ns("add") devient "counter-add" — c’est ce qui munit l’UI d’un espace de noms. moduleServer(id, function(input, output, session) { ... }) est utilisé dans le serveur : il exécute votre logique dans ce même espace de noms, où vous utilisez des ids simples. Espace de noms de l’UI = NS() ; logique serveur = moduleServer().
Oui. Le serveur d’un module peut appeler le serveur d’un autre module, et son UI peut inclure l’UI d’un autre module — vous les imbriquez comme des fonctions. Les espaces de noms se composent automatiquement : un enfant enregistré avec childServer("sub") à l’intérieur d’un parent enregistré comme "parent" vit sous parent-sub, si bien que les ids n’entrent jamais en collision, même à plusieurs niveaux de profondeur. L’imbrication, c’est ainsi qu’une grande application devient un arbre de petits morceaux testables indépendamment.
Testez vos connaissances
Vous avez une application avec deux blocs saisie-de-texte-et-écho quasi identiques, renommés à la main pour éviter les conflits d’ids :
ui <- fluidPage(
textInput("name1", "Name"), textOutput("greeting1"),
textInput("name2", "Name"), textOutput("greeting2")
)
server <- function(input, output, session) {
output$greeting1 <- renderText(paste("Hello", input$name1))
output$greeting2 <- renderText(paste("Hello", input$name2))
}Réécrivez ceci sous forme de module (greetUI / greetServer) utilisé deux fois. Écrivez les deux fonctions et l’application qui les appelle avec les ids "one" et "two".
La fonction UI prend un id, construit ns <- NS(id) et enveloppe à la fois les ids textInput et textOutput dans ns(). La fonction serveur passe id à moduleServer() et, à l’intérieur, utilise les ids simples — input$name et output$greeting — sans préfixe d’espace de noms. Ensuite, appelez greetUI("one") / greetUI("two") dans l’UI et greetServer("one") / greetServer("two") dans le serveur.
library(shiny)
greetUI <- function(id) {
ns <- NS(id)
tagList(
textInput(ns("name"), "Name"),
textOutput(ns("greeting"))
)
}
greetServer <- function(id) {
moduleServer(id, function(input, output, session) {
output$greeting <- renderText(paste("Hello", input$name))
})
}
ui <- fluidPage(
greetUI("one"),
greetUI("two")
)
server <- function(input, output, session) {
greetServer("one")
greetServer("two")
}
shinyApp(ui, server)Le composant est écrit une seule fois. L’instance "one" possède les ids "one-name" / "one-greeting" et l’instance "two" possède "two-name" / "two-greeting" — aucune collision, rien de renommé. Ajouter un troisième message d’accueil ne fait plus qu’une ligne par moitié. Notez que ns() n’enveloppe les ids que dans greetUI ; à l’intérieur de greetServer, les ids sont simples (input$name, output$greeting).
Un module utilisé avec l’id "counter" a un bouton créé dans son UI avec actionButton(ns("add"), "Add"). À l’intérieur de moduleServer(), comment désignez-vous la valeur de ce bouton ?
A. input$`counter-add` — vous devez inclure le préfixe d’espace de noms.
B. input$add — à l’intérieur du serveur de module, vous utilisez l’id simple, non préfixé.
C. input$ns("add") — vous rappelez ns() dans le serveur.
D. session$add — les entrées du module vivent sur l’objet session.
B. À l’intérieur de moduleServer(), vous utilisez l’id simple, input$add. Le préfixage par NS() ("counter-add") est appliqué dans l’UI et par la plomberie de Shiny ; le serveur du module s’exécute au sein de son espace de noms, il voit donc des ids non préfixés — c’est pourquoi la logique serveur se lit exactement comme une petite application autonome. A est l’erreur classique : input$`counter-add` ne renvoie à rien depuis l’intérieur du module. C emploie mal ns() (il appartient à l’UI). D est inventé — les entrées se lisent toujours depuis input, non depuis session.
Conclusion
Un module Shiny est une paire UI + serveur que vous écrivez une fois et réutilisez partout, isolée par un espace de noms. NS(id) préfixe chaque id de l’UI pour que les instances n’entrent jamais en collision ; moduleServer() contient la logique, où vous utilisez des ids simples non préfixés ; les modules communiquent en renvoyant des réactifs vers le haut et en passant des réactifs vers le bas, jamais en accédant à travers les espaces de noms ; et testServer() prouve la logique d’un module sans navigateur, si bien que le composant est digne de confiance à lui seul. C’est la primitive de réutilisation sur laquelle repose le reste de l’architecture Shiny — c’est ce que golem génère avec add_module(), et l’unité sur laquelle grandit une application maintenable. Écrivez le composant une fois, réutilisez-le en confiance, et laissez le runtime être le juge.
Leçons connexes
- Construire sur et autour des modules : la leçon sur la programmation réactive couvre les fondations
reactiveVal()/observeEvent()/reactive()dont dépendent les modules — lisez-la d’abord si la chaîne réactive ici vous a semblé fragile. La leçon sur le framework golem montre commentadd_module()échafaude exactement la paireNS()/moduleServer()que vous venez de construire, de façon cohérente et à grande échelle ; et les leçons sur l’organisation du code et le développement de packages de cette série couvrent l’emplacement réel des fichiers de module dans un projet. - Aller plus loin : Applications web Shiny — tout le cursus Shiny. · Science computationnelle et IA — le pilier.
Prouvez que vous savez le faire. Maîtrisez toute la série Architecture Shiny — suivez votre parcours, construisez des projets et obtenez un certificat.
Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.
dès 15 $/mois facturé annuellement
✓ Vous êtes Pro — continuez. The runtime is the judge.
Réutilisation
Citation
@online{2026,
author = {},
title = {Modules Shiny : composants UI + serveur réutilisables},
date = {2026-07-16},
url = {https://www.datanovia.com/learn/programming/shiny/architecture/modules},
langid = {fr}
}