UI dynamique Shiny : renderUI, uiOutput, updateSelectInput & insertUI

Construisez une interface qui change à l’exécution — générez un contrôle avec renderUI/uiOutput, modifiez-en un existant avec updateXxxInput, et ajoutez ou supprimez une UI répétée avec insertUI/removeUI.

Programming
Shiny

Parfois ce sont les contrôles eux-mêmes qui doivent changer — un second menu dont les choix dépendent du premier, un formulaire qui gagne une ligne quand l’utilisateur clique sur « ajouter ». Cette leçon couvre les trois outils de l’UI à l’exécution : renderUI() + uiOutput() pour construire un contrôle côté server et le déposer dans la page, updateXxxInput() pour modifier un contrôle existant sans le reconstruire, et insertUI()/removeUI() pour ajouter ou supprimer une UI quelconque au niveau d’un sélecteur. Plus req() pour protéger l’état vide, comment lire des entrées créées dynamiquement, et une règle claire pour savoir quel outil choisir et quand.

Date de publication

28 juin 2026

Modifié

7 juillet 2026

AstucePoints clés
  • Trois tâches, trois outils. Construire un contrôle de zéro côté server → renderUI() + uiOutput(). Modifier un contrôle qui existe déjà → updateXxxInput(). Ajouter ou supprimer des blocs d’UI répétés → insertUI() / removeUI().
  • renderUI() + uiOutput() est l’outil de référence. Placez un emplacement réservé uiOutput("id") dans l’UI, construisez le contrôle à l’intérieur de output$id <- renderUI({ ... }) côté server. L’id de la sortie et l’id de l’emplacement réservé doivent correspondre.
  • updateXxxInput() est l’option plus légère — préférez-la quand le contrôle existe déjà. updateSelectInput(), updateSliderInput(), updateTextInput() et consorts modifient la valeur, les choix ou l’étiquette d’un contrôle sur place sans le reconstruire. Ils ont besoin de l’objet session.
  • insertUI() / removeUI() est l’option chirurgicale. Injectez ou supprimez une UI quelconque au niveau d’un sélecteur CSS à l’exécution — la façon de construire des formulaires « ajouter une autre ligne ».
  • Protégez l’état vide avec req(). Une entrée créée dynamiquement vaut NULL jusqu’à ce qu’elle existe, donc req(input$col) empêche un bloc de planter avant que le contrôle ait été construit.
  • Lisez une entrée générée par son nom. Les entrées que vous créez dans une boucle se lisent avec input[[paste0("x", i)]], pas input$x1.

Introduction

Jusqu’ici, chaque application changeait ses sorties en réponse à une entrée — un nouveau graphique, un tableau frais. Parfois ce sont les contrôles eux-mêmes qui doivent changer. Un sélecteur de colonne dont les choix dépendent du jeu de données que l’utilisateur vient de choisir. Un second menu qui n’a de sens qu’après le premier. Un formulaire qui gagne une nouvelle ligne chaque fois que l’utilisateur clique sur « ajouter une autre ». L’interface ne peut pas être entièrement écrite à l’avance, car sa forme dépend des données ou de ce que fait l’utilisateur.

Shiny vous donne trois façons de changer l’UI à l’exécution, et tout le savoir-faire consiste à choisir la bonne :

  • renderUI() + uiOutput() — construire un contrôle (ou tout un bloc de contrôles) côté server et le déposer dans un emplacement réservé de la page. L’outil de référence : utilisez-le quand l’existence ou la structure d’un contrôle dépend de l’état.
  • updateXxxInput() — modifier un contrôle qui est déjà dans la page (sa valeur, ses choix ou son étiquette) sans le reconstruire. L’option plus légère : préférez-la chaque fois que le contrôle existe déjà.
  • insertUI() / removeUI() — ajouter ou supprimer une UI quelconque au niveau d’un sélecteur. L’option plus lourde et chirurgicale pour les motifs répétés « ajouter une ligne ».
NoteCopiez n’importe quel bloc dans app.R et exécutez-le en local

Le code de cette page n’est pas exécuté ici — une application Shiny a besoin d’une session R vivante, elle ne peut donc pas tourner dans une page web statique. Copiez n’importe quel bloc dans un fichier app.R et exécutez-le en local avec shiny::runApp() (ou cliquez sur Run App dans RStudio). Les exemples utilisent du R de base et des jeux de données intégrés comme mtcars et iris, ils tournent donc tels quels.

renderUI() et uiOutput() : construire un contrôle côté server

renderUI() est l’outil de référence. Vous laissez un emplacement réservé dans l’UI avec uiOutput("id"), puis vous construisez le vrai contrôle côté server à l’intérieur de output$id <- renderUI({ ... }). Quelle que soit l’UI que vous y renvoyez, elle est rendue dans l’emplacement réservé. Le cas classique est un second menu dont les choix dépendent d’un premier menu.

Ici un selectInput choisit un jeu de données, et un second select — construit avec renderUI() — propose les colonnes du jeu de données choisi :

library(shiny)

ui <- fluidPage(
  selectInput("dataset", "Dataset:", choices = c("mtcars", "iris")),
  uiOutput("col_picker"),          # placeholder — the real control is built on the server
  verbatimTextOutput("summary")
)

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

  # the chosen data frame
  data <- reactive({ get(input$dataset) })

  # build a selectInput whose choices are this dataset's columns
  output$col_picker <- renderUI({
    selectInput("col", "Column:", choices = names(data()))
  })

  output$summary <- renderText({
    req(input$col)                 # wait until the dynamic control exists
    paste(capture.output(summary(data()[[input$col]])), collapse = "\n")
  })
}

shinyApp(ui, server)

Le uiOutput("col_picker") dans l’UI est un emplacement vide. Côté server, output$col_picker <- renderUI({...}) renvoie un selectInput("col", ...) dont les choices sont les noms de colonnes du jeu de données sélectionné — ainsi, quand l’utilisateur passe de mtcars à iris, le menu de colonnes se reconstruit avec les colonnes d’iris. L’id dans uiOutput("col_picker") doit correspondre à l’id dans output$col_picker — c’est cet appariement qui dit à Shiny où placer l’UI générée. Le contrôle que crée renderUI() (input$col) se lit comme n’importe quelle autre entrée.

renderUI() peut renvoyer plus d’un contrôle — enveloppez-en plusieurs dans tagList() et tout le bloc apparaît d’un coup :

output$controls <- renderUI({
  tagList(
    sliderInput("n", "Rows to show:", min = 1, max = nrow(data()), value = 5),
    checkboxInput("desc", "Descending", value = FALSE)
  )
})

Choisissez renderUI() chaque fois que l’existence ou la structure du contrôle dépend de l’état — des jeux de données différents ont besoin de menus de colonnes différents, un choix « régression » a besoin d’entrées différentes d’un choix « résumé ». Il reconstruit le contrôle de zéro chaque fois que ses dépendances réactives changent, ce qui est aussi son coût (voir Problèmes courants).

updateXxxInput() : modifier un contrôle qui existe déjà

Si le contrôle est déjà dans la page et que vous devez seulement changer sa valeur, ses choix ou son étiquette, ne le reconstruisez pas avec renderUI() — mettez-le à jour sur place. Chaque entrée a une fonction update* correspondante : updateSelectInput(), updateSliderInput(), updateTextInput(), updateNumericInput(), updateCheckboxInput(), et ainsi de suite. Elles prennent l’objet session, l’id de l’entrée et les éléments à changer.

C’est la façon plus légère de réaliser le motif jeu-de-données-puis-colonne ci-dessus. Le selectInput de colonne est maintenant une partie statique de l’UI ; un observeEvent() se contente d’échanger ses choix quand le jeu de données change :

library(shiny)

ui <- fluidPage(
  selectInput("dataset", "Dataset:", choices = c("mtcars", "iris")),
  selectInput("col", "Column:", choices = NULL),   # exists from the start, filled later
  verbatimTextOutput("summary")
)

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

  data <- reactive({ get(input$dataset) })

  # change the EXISTING select's choices in place — no rebuild
  observeEvent(input$dataset, {
    updateSelectInput(session, "col", choices = names(data()))
  })

  output$summary <- renderText({
    req(input$col)
    paste(capture.output(summary(data()[[input$col]])), collapse = "\n")
  })
}

shinyApp(ui, server)

updateSelectInput(session, "col", choices = names(data())) atteint le contrôle col vivant et remplace ses choix — le widget reste en place, seules ses options changent. Comme le contrôle n’est pas recréé, l’utilisateur conserve le focus et tout état sans rapport, et il n’y a pas de scintillement. Notez que le premier argument est session : les fonctions de mise à jour transmettent un message au navigateur via la session, elles ne fonctionnent donc qu’à l’intérieur du server (et la fonction server doit accepter session).

La même idée définit une valeur (updateSliderInput(session, "n", value = 10)), une étiquette (updateTextInput(session, "name", label = "Full name")), ou sélectionne le choix courant (updateSelectInput(session, "col", selected = "mpg")). Préférez updateXxxInput() chaque fois que le contrôle existe déjà — ne reconstruisez pas ce que vous pouvez mettre à jour.

insertUI() et removeUI() : ajouter et supprimer une UI au niveau d’un sélecteur

renderUI() remplace le contenu d’un seul emplacement réservé. Quand vous devez ajouter une UI à côté d’une UI existante — sans détruire ce qui est déjà là — utilisez insertUI(). Il injecte un bloc d’UI au niveau d’un sélecteur CSS, et removeUI() supprime l’UI qui correspond à un sélecteur. C’est ainsi que vous construisez des formulaires « ajouter une autre ligne ».

Ici chaque clic sur un actionButton insère une nouvelle entrée texte ; les entrées s’empilent au lieu de se remplacer les unes les autres :

library(shiny)

ui <- fluidPage(
  actionButton("add", "Add a field"),
  div(id = "fields")               # the container we insert into
)

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

  observeEvent(input$add, {
    insertUI(
      selector = "#fields",        # where to insert (a CSS selector)
      where = "beforeEnd",         # append inside the container
      ui = textInput(
        inputId = paste0("field_", input$add),   # unique id per click
        label = paste("Field", input$add)
      )
    )
  })
}

shinyApp(ui, server)

Chaque clic exécute l’observateur, et insertUI() ajoute un textInput frais à l’intérieur du div #fields. Le where = "beforeEnd" le place à la fin des enfants du conteneur (les autres options sont "beforeBegin", "afterBegin", "afterEnd"). Donnez à chaque contrôle inséré un id unique — ici paste0("field_", input$add) utilise le compteur de clics, donc les ids sont field_1, field_2, … removeUI(selector = "#fields .form-group:last-child") supprimerait la plus récente.

insertUI() est le plus lourd des trois et le plus manuel — vous gérez vous-même les sélecteurs et les ids — alors choisissez-le seulement quand vous avez vraiment besoin d’ajouter ou supprimer des blocs répétés qui coexistent. Pour un seul contrôle qui change selon l’état, renderUI() ou updateXxxInput() est plus simple.

req() : protéger l’état vide

Un contrôle construit par renderUI() ou insertUI() n’existe pas au premier chargement de l’application — il est créé un instant plus tard, après l’exécution du server. Jusque-là, l’entrée qu’il définit se lit comme NULL. Tout bloc qui l’utilise plantera à ce premier passage à moins que vous ne le protégiez.

req() est la protection. req(input$col) arrête le réactif jusqu’à ce que input$col ait une valeur réelle (truthy), de sorte que la sortie reste vide au lieu de lancer une erreur rouge avant que le contrôle dynamique ait été construit :

output$summary <- renderText({
  req(input$col)                   # NULL until the dynamic select exists -> wait
  paste(capture.output(summary(data()[[input$col]])), collapse = "\n")
})

Sans req(input$col), data()[[input$col]] s’exécute avec input$col égal à NULL au premier rendu et plante. Avec lui, le bloc attend simplement — dès que renderUI() crée le contrôle et que l’utilisateur choisit une colonne, input$col devient truthy et la sortie se calcule. Protégez chaque lecture d’une entrée créée dynamiquement de cette façon. (req() est traité en détail dans la logique conditionnelle.)

Lire des entrées créées dynamiquement

Quand vous créez des entrées dans une boucle — disons une entrée numérique par colonne — leurs ids sont construits comme des chaînes, donc vous ne pouvez pas les atteindre avec input$x1. Utilisez la forme à crochets input[[id]] avec la même chaîne à partir de laquelle vous avez construit l’id :

# build n numeric inputs
output$weights <- renderUI({
  cols <- names(mtcars)
  lapply(cols, function(nm) {
    numericInput(paste0("w_", nm), paste("Weight for", nm), value = 1)
  })
})

# read them back by name
output$total <- renderText({
  cols <- names(mtcars)
  vals <- sapply(cols, function(nm) {
    w <- input[[paste0("w_", nm)]]   # input[[id]], not input$w_mpg
    if (is.null(w)) 0 else w
  })
  paste("Sum of weights:", sum(vals))
})

input[[paste0("w_", nm)]] cherche l’entrée par son nom calculé — la seule façon de lire un contrôle dont vous avez généré l’id par programmation. Le if (is.null(w)) 0 maintient la somme fonctionnelle au premier rendu, avant que les entrées générées existent (le même problème d’état vide que req() résout, traité en ligne ici pour que le total affiche zéro plutôt que d’attendre).

Quel outil et quand

Les trois changent l’UI à l’exécution ; ils diffèrent selon que le contrôle existe déjà et selon la quantité de reconstruction qu’ils font.

Outil Choisissez-le quand
updateXxxInput() Le contrôle existe déjà et vous devez seulement changer sa valeur, ses choix ou son étiquette. L’option la plus légère — pas de reconstruction, pas de focus perdu.
renderUI() + uiOutput() L’existence ou la structure du contrôle dépend de l’état — un ensemble de contrôles différent pour un choix ou un jeu de données différent. Reconstruit de zéro.
insertUI() / removeUI() Vous devez ajouter ou supprimer des blocs répétés d’UI qui coexistent — un formulaire « ajouter une autre ligne ». Le plus lourd et le plus manuel.

La décision en une ligne : si le contrôle est déjà là, faites-en un updateXxxInput() ; si son existence même ou sa forme dépend de l’état, faites-en un renderUI() ; si vous agrandissez ou réduisez une liste de contrôles, insertUI() / removeUI(). renderUI() est plus lourd qu’une mise à jour parce qu’il reconstruit le widget chaque fois que ses dépendances changent — alors ne reconstruisez pas ce que vous pouvez mettre à jour.

Problèmes fréquents

renderUI() reconstruit le contrôle et perd l’état ou le focus. Chaque fois que les dépendances réactives d’un bloc renderUI() changent, il recrée le contrôle de zéro — ce qui réinitialise la sélection de l’utilisateur, efface le texte saisi et fait perdre le focus du clavier. Si le contrôle est déjà dans la page et que vous devez seulement changer sa valeur ou ses choix, utilisez plutôt updateXxxInput() : il modifie le widget vivant sur place, de sorte que l’utilisateur conserve sa place. Reconstruisez avec renderUI() seulement quand la structure du contrôle doit réellement changer.

Le contrôle dynamique n’apparaît jamais. L’id dans uiOutput("col_picker") doit correspondre exactement à l’id dans output$col_picker <- renderUI({...}) — ce sont l’emplacement réservé et ce qui le remplit. Une faute de frappe ou un nom non concordant (uiOutput("colpicker") vs output$col_picker) laisse l’emplacement vide sans erreur. Vérifiez que les deux ids sont identiques.

Une entrée dynamique se lit comme NULL et le bloc plante. Un contrôle construit par renderUI() ou insertUI() n’existe pas au premier rendu, donc l’entrée qu’il définit est NULL jusque-là. La lire (data()[[input$col]]) plante avant que le contrôle soit construit. Placez req(input$col) en haut du bloc pour qu’il attende l’existence de l’entrée au lieu de planter.

Questions fréquentes

Placez un emplacement réservé uiOutput("id") dans l’UI, puis construisez le contrôle côté server avec output$id <- renderUI({ ... }), en renvoyant une entrée Shiny (ou un tagList() de plusieurs). Quoi que vous renvoyiez, c’est rendu dans l’emplacement réservé, et son id doit correspondre. Choisissez cela quand l’existence ou la structure d’un contrôle dépend de l’état — par exemple un menu de colonnes dont les choix viennent du jeu de données sélectionné.

renderUI() construit un contrôle (ou un bloc de contrôles) de zéro et le dépose dans un emplacement réservé uiOutput() — utilisez-le quand l’existence ou la structure du contrôle dépend de l’état. updateSelectInput() (et les autres fonctions updateXxxInput()) modifient un contrôle existant — sa valeur, ses choix ou son étiquette sur place, sans le reconstruire. Préférez la mise à jour quand le contrôle existe déjà : elle est plus légère et conserve le focus et la sélection de l’utilisateur.

Utilisez insertUI() pour injecter une UI au niveau d’un sélecteur CSS et removeUI() pour supprimer l’UI correspondant à un sélecteur — la façon de construire des formulaires « ajouter une autre ligne ». Dans un observeEvent(), appelez insertUI(selector = "#container", where = "beforeEnd", ui = ...) pour ajouter un nouveau contrôle à l’intérieur de #container, en donnant à chacun un id unique (p. ex. paste0("field_", input$add)). removeUI(selector = ...) supprime l’élément correspondant.

Si le contrôle existe déjà, appelez updateSelectInput(session, "col", choices = new_choices) depuis l’intérieur d’un observeEvent() ou observe() — il remplace les choix sur place sans reconstruire le widget. Le premier argument doit être session. Si l’existence même du menu de colonnes dépend d’une autre entrée, construisez-le plutôt avec renderUI().

Utilisez la forme à crochets input[[id]] avec la chaîne à partir de laquelle vous avez construit l’id — input[[paste0("w_", nm)]], pas input$w_mpg. Les entrées créées dynamiquement valent NULL jusqu’à ce qu’elles existent, alors protégez chaque lecture avec req() (ou une vérification is.null()) pour que le bloc attende au lieu de planter au premier rendu.

Testez vos connaissances

Construisez une application avec un selectInput("dataset", ...) proposant "mtcars" et "iris", plus un second select pour les colonnes du jeu de données choisi. Quand l’utilisateur change de jeu de données, le menu de colonnes doit se mettre à jour vers les colonnes de ce jeu. Affichez ensuite le summary() de la colonne sélectionnée. Faites-le de deux façons et remarquez la différence : une fois avec renderUI() + uiOutput(), une fois avec un selectInput("col", choices = NULL) statique mis à jour par updateSelectInput().

Pour la version renderUI() : un uiOutput("col_picker") dans l’UI, et côté server output$col_picker <- renderUI({ selectInput("col", "Column:", choices = names(data())) }). Pour la version mise à jour : un selectInput("col", choices = NULL) dans l’UI et observeEvent(input$dataset, { updateSelectInput(session, "col", choices = names(data())) }). Dans les deux cas, req(input$col) avant de la lire.

library(shiny)

# --- Version A: renderUI() + uiOutput() ---
ui <- fluidPage(
  selectInput("dataset", "Dataset:", choices = c("mtcars", "iris")),
  uiOutput("col_picker"),
  verbatimTextOutput("summary")
)

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

  data <- reactive({ get(input$dataset) })

  output$col_picker <- renderUI({
    selectInput("col", "Column:", choices = names(data()))
  })

  output$summary <- renderText({
    req(input$col)
    paste(capture.output(summary(data()[[input$col]])), collapse = "\n")
  })
}

shinyApp(ui, server)

# --- Version B: updateSelectInput() (control already exists) ---
ui <- fluidPage(
  selectInput("dataset", "Dataset:", choices = c("mtcars", "iris")),
  selectInput("col", "Column:", choices = NULL),
  verbatimTextOutput("summary")
)

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

  data <- reactive({ get(input$dataset) })

  observeEvent(input$dataset, {
    updateSelectInput(session, "col", choices = names(data()))
  })

  output$summary <- renderText({
    req(input$col)
    paste(capture.output(summary(data()[[input$col]])), collapse = "\n")
  })
}

shinyApp(ui, server)

Les deux produisent le même comportement. La version A reconstruit le select de colonne chaque fois que le jeu de données change ; la version B garde un seul select et change ses choix sur place — plus légère, et le widget ne scintille jamais. Préférez B quand le contrôle existe toujours ; choisissez A seulement quand la structure du contrôle doit changer.

Vous avez un selectInput("col", ...) déjà dans votre UI, et vous voulez changer ses choix quand l’utilisateur choisit un jeu de données différent. Quel est le bon outil ?

A. Le reconstruire avec renderUI() + uiOutput() à chaque changement de jeu de données. B. updateSelectInput(session, "col", choices = new_choices) à l’intérieur d’un observeEvent(). C. insertUI() un nouveau selectInput chaque fois que le jeu de données change. D. Rien — les choix d’un selectInput ne peuvent pas changer une fois qu’il est créé.

B. Le contrôle existe déjà, alors mettez-le à jour sur place avec updateSelectInput() — il échange les choix sans reconstruire le widget, de sorte que l’utilisateur conserve le focus et qu’il n’y a pas de scintillement. A fonctionne mais est plus lourd (renderUI() reconstruit tout le contrôle) ; C empilerait des menus en double ; D est faux — les choix changent librement via update*. Préférez la mise à jour chaque fois que le contrôle existe déjà.

Conclusion

L’UI dynamique est la dernière pièce de la série interactivité, et tout se ramène à une seule décision : le contrôle existe-t-il déjà ? Si oui, modifiez-le sur place avec updateXxxInput() — le geste le plus léger, pas de reconstruction, pas de focus perdu. Si son existence ou sa structure dépend de l’état, construisez-le côté server avec renderUI() et un emplacement réservé uiOutput() correspondant. Si vous agrandissez ou réduisez une liste de contrôles, insertUI() / removeUI() ajoutent et suppriment une UI au niveau d’un sélecteur. Quel que soit votre choix, protégez l’état vide avec req() (une entrée dynamique vaut NULL jusqu’à ce qu’elle existe) et lisez les entrées générées par leur nom avec input[[id]]. Avec cela, l’interface elle-même devient réactive — des contrôles qui apparaissent, changent et s’adaptent aux données et à l’utilisateur.

Cela complète la série Interactivité Shiny. Des mises à jour en temps réel à l’UI dynamique, vous pouvez désormais réaliser une application qui s’actualise d’elle-même et remodèle sa propre interface à l’exécution.

Leçons connexes

Cette page vous a-t-elle été utile ?

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {UI dynamique Shiny : renderUI, uiOutput, updateSelectInput \&
    insertUI},
  date = {2026-06-28},
  url = {https://www.datanovia.com/learn/programming/shiny/interactivity/dynamic-ui},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“UI dynamique Shiny : renderUI, uiOutput, updateSelectInput & insertUI.” 2026. June 28. https://www.datanovia.com/learn/programming/shiny/interactivity/dynamic-ui.