Organisation du code Shiny : structurer un app.R en fichiers R/

Découpez un app.R monolithique en un dossier R/ que vous chargez avec source(), un global.R pour le démarrage et une convention de nommage claire — sans avoir besoin d’un package.

Programming
Shiny

Divisez un app.R Shiny qui grossit en une structure de fichiers maintenable sans en faire un package. Extrayez les fonctions auxiliaires dans un dossier R/ et chargez-les avec source(), déplacez la configuration ponctuelle dans global.R, séparez le ui et le server dans leurs propres fichiers, et adoptez la convention de nommage mod_/fct_/utils_. La première étape, sans package, vers l’architecture Shiny.

Date de publication

16 juillet 2026

Modifié

16 juillet 2026

AstucePoints clés
  • Découpez en déplaçant le code dans un dossier R/ et en le chargeant avec source(). Une fonction auxiliaire extraite de app.R vers R/fct_summary.R, puis chargée à nouveau, s’exécute exactement comme avant — vous avez réorganisé les fichiers, pas modifié l’application.
  • global.R sert au code qui s’exécute une seule fois au démarrage, partagé à la fois par ui et server : charger les packages, lire les données, définir les options. Il s’exécute une seule fois au lancement de l’application, pas une fois par session utilisateur.
  • Une convention de nommage rend un dossier navigable. Le standard de fait issu de golem : mod_*.R pour les modules Shiny, fct_*.R pour la logique métier, utils_*.R pour les petites fonctions auxiliaires, app_ui.R / app_server.R pour le ui et le server séparés.
  • Vous n’avez pas encore besoin d’un package. C’est la première étape — un dossier organisé de fichiers sourcés. En faire un package (et laisser golem tout automatiser) sont les étapes suivantes, pas celle-ci.
  • The runtime is the judge. L’application découpée renvoie le même shiny.appobj et calcule le même résultat que la version en un seul fichier — une restructuration qui change le comportement est un bug, et l’exécuter est justement la façon de le détecter.

Introduction

Votre application Shiny a commencé comme un unique app.R, et c’était très bien. Puis elle a grossi. Elle fait maintenant plusieurs centaines de lignes, chaque modification oblige à faire défiler du code sans rapport pour trouver le bloc voulu, et un collègue qui l’ouvre ne peut pas localiser la logique du graphique sans une recherche. Le fichier fonctionne toujours — il est simplement devenu difficile d’y travailler.

La solution n’est pas une réécriture. Vous découpez le fichier unique en un dossier de fichiers plus petits, vous les rechargez ensemble avec source(), et vous donnez à chaque fichier un rôle que son nom laisse deviner. C’est tout. Vous obtenez une base de code navigable, que plusieurs personnes peuvent modifier sans se marcher dessus, sans transformer votre application en package R — c’est une étape ultérieure, traitée dans sa propre leçon. C’est la première étape : le plus petit changement qui vous apporte une vraie organisation.

Quand ne pas s’en soucier. Une application de 60 lignes est parfaitement bien dans un seul app.R. La découper ajoute des fichiers à ouvrir et une boucle source() à maintenir sans aucun gain — c’est de la sur-ingénierie. Adoptez une structure quand le fichier a dépassé ce que vous gardez confortablement en tête, quand vous vous répétez, ou quand plus d’une personne le modifie. En dessous de ce seuil, laissez-le en un seul fichier.

NotePrérequis

Il vous faut une application Shiny fonctionnelle que vous aimeriez ranger, et de l’aisance à écrire de simples fonctions R — toute la technique se résume à « extraire un morceau de logique dans une fonction, puis placer la fonction dans son propre fichier ». Tout ici repose sur Shiny de base ; aucun package supplémentaire :

install.packages("shiny")
NoteCopiez n’importe quel bloc et exécutez-le en local

Les blocs ci-dessous écrivent des fichiers sur le disque et lancent un serveur en direct, donc rien sur cette page n’est exécuté ici — une page web statique ne peut pas créer un dossier R/ ni démarrer une application Shiny. Copiez n’importe quel bloc sur votre machine et exécutez-le pour les reproduire. Ce qu’il s’agit de prouver, c’est que la réorganisation ne change rien au comportement : l’application découpée renvoie le même shiny.appobj, et la fonction auxiliaire que vous avez déplacée calcule le même nombre qu’en ligne. The runtime is the judge.

D’un fichier unique à un dossier R/

Partez d’un monolithe. Voici une petite application qui affiche la moyenne des miles par gallon du jeu de données intégré mtcars — le calcul est écrit directement dans le server :

# app.R — everything in one file
library(shiny)

ui <- fluidPage(
  textOutput("avg")
)

server <- function(input, output, session) {
  output$avg <- renderText({
    paste("Average mpg:", round(mean(mtcars$mpg), 2))
  })
}

shinyApp(ui, server)

Extrayez maintenant le calcul. Créez un dossier nommé R/ à côté de app.R, et placez la logique dans son propre fichier sous forme de fonction nommée :

# R/fct_summary.R
summarise_mpg <- function(data) {
  mean(data$mpg)
}

De retour dans app.R, chargez tous les fichiers de R/ en haut du fichier avec une petite boucle source(), puis appelez la fonction là où se trouvait le code en ligne :

# app.R — the calculation now lives in R/fct_summary.R
library(shiny)

# load every file in R/ so its functions are available
for (f in list.files("R", full.names = TRUE)) source(f)

ui <- fluidPage(
  textOutput("avg")
)

server <- function(input, output, session) {
  output$avg <- renderText({
    paste("Average mpg:", round(summarise_mpg(mtcars), 2))
  })
}

shinyApp(ui, server)

list.files("R", full.names = TRUE) renvoie les chemins de chaque fichier du dossier, et la boucle for charge chacun avec source() — de sorte que summarise_mpg() est défini au moment où server s’exécute. L’application construit le même shiny.appobj qu’avant, et summarise_mpg(mtcars) renvoie 20.09 qu’il soit en ligne ou chargé. Vous avez déplacé du code entre des fichiers ; le comportement est identique. C’est toute l’idée, et chaque étape ci-dessous en est une variation.

Votre projet ressemble maintenant à ceci — montré avec fs::dir_tree(), qui affiche un répertoire sous forme d’arbre en texte :

fs::dir_tree("myapp")
myapp
├── app.R              # entry point: sources R/, defines ui + server, calls shinyApp()
└── R
    └── fct_summary.R  # summarise_mpg() — one job, easy to find

À mesure que l’application grossit, vous ajoutez d’autres fichiers dans R/ — un par module, un par groupe de fonctions liées — et la boucle source() les récupère tous automatiquement. Vous ne touchez plus jamais à la boucle ; vous déposez simplement des fichiers.

global.R — le code qui s’exécute une seule fois au démarrage

Une partie de la configuration ne devrait pas se trouver dans server du tout : charger les packages, lire un jeu de données, définir les options. Elle est identique pour chaque utilisateur et n’a besoin de se produire qu’une seule fois. C’est à cela que sert global.R. Shiny traite spécialement un fichier nommé global.R situé à côté de votre app.R — il est évalué une seule fois au démarrage de l’application, et tout ce qu’il définit est visible à la fois par le ui et le server. L’article sur les règles de portée de Shiny documente ce comportement.

# global.R — evaluated once at startup, shared by ui and server
library(shiny)

cars_data <- mtcars            # read data once, reuse across all sessions
options(digits = 4)            # app-wide options

# load your helper files here too, if you prefer keeping the loop out of app.R
for (f in list.files("R", full.names = TRUE)) source(f)

La distinction qui compte : global.R s’exécute une seule fois pour toute l’application, pas une fois par utilisateur. Tout ce qui est coûteux et partagé — lire un fichier, ouvrir une connexion, charger un gros objet — a sa place ici, pour que chaque session le réutilise au lieu de refaire le travail. Le code qui doit être propre à chaque utilisateur (tout ce qui est réactif, tout ce qui dépend de input) reste à l’intérieur de server, que Shiny réexécute pour chaque visiteur.

Alors, où placer le chargement de R/ — dans global.R ou en haut de app.R ? Les deux fonctionnent ; choisissez-en un et soyez cohérent. Placer la boucle source() dans global.R garde app.R concentré sur ui, server et shinyApp() uniquement. (Shiny récent charge en fait automatiquement global.R et les fichiers de R/ pour vous — aussi bien en local qu’une fois déployé sur shinyapps.io ou sur Shiny Server — mais écrire la boucle explicite est la manière portable et transparente qui fonctionne sur toutes les versions de Shiny.)

Une convention de nommage qui passe à l’échelle

Dès que R/ contient plus de quelques fichiers, une convention de nommage le transforme d’un fourre-tout en une carte. Le standard largement adopté vient de golem et du livre qui l’accompagne, Engineering Production-Grade Shiny Apps — vous pouvez lire les noms de fichiers et savoir ce qu’ils contiennent avant même d’ouvrir quoi que ce soit :

Préfixe Contient Exemple
mod_*.R Un module Shiny — une paire UI + server réutilisable et à espace de noms mod_filters.R
fct_*.R Fonctions de logique métier (calcul, transformation, analyse) fct_summary.R
utils_*.R Petites fonctions auxiliaires génériques utilisées dans toute l’application utils_format.R
app_ui.R La définition de l’UI de haut niveau
app_server.R La définition du server de haut niveau
global.R Code qui s’exécute une seule fois au démarrage, partagé par ui + server

Le fichier fct_ que vous avez déjà créé respecte la convention. Quand un morceau d’UI commence à se répéter — le même onglet deux fois, un contrôle que vous voulez à deux endroits — c’est le signal qu’il faut en faire un module et le nommer mod_*.R. Les modules sont un sujet à part entière (l’espace de noms NS() / moduleServer() qui vous permet de réutiliser un composant sans collision d’identifiants) ; la leçon sur les modules Shiny de cette série les traite en détail. Ici, la seule chose à savoir est où vit un fichier de module et comment il s’appelle : dans R/, préfixé par mod_.

Séparer ui et server

La dernière découpe courante concerne le ui et le server eux-mêmes. Quand la seule définition de l’UI dépasse la centaine de lignes, extrayez chacun dans son propre fichier — R/app_ui.R et R/app_server.R — pour que app.R devienne un court lanceur :

# R/app_ui.R
app_ui <- function() {
  fluidPage(
    textOutput("avg")
  )
}
# R/app_server.R
app_server <- function(input, output, session) {
  output$avg <- renderText({
    paste("Average mpg:", round(summarise_mpg(mtcars), 2))
  })
}
# app.R — a thin entry point
library(shiny)
for (f in list.files("R", full.names = TRUE)) source(f)

shinyApp(ui = app_ui(), server = app_server)

app.R ne fait plus qu’une seule chose — relier les morceaux entre eux — et chaque grande définition a sa propre place. Notez que app_ui est écrit comme une fonction que vous appelez (app_ui()), ce qui correspond à la forme attendue par golem et les applications basées sur un package ; un simple objet ui fonctionne tout aussi bien si vous préférez.

Vers quoi cela mène

Vous avez maintenant un dossier organisé : app.R comme lanceur, un dossier R/ de fichiers nommés, global.R pour le démarrage, et une convention que tout le monde peut lire. Pour beaucoup d’applications, c’est la ligne d’arrivée. Deux étapes supplémentaires s’appuient directement dessus quand vous en avez besoin :

  • Faites-en un package. Envelopper l’application dans un package R ajoute un DESCRIPTION (une liste lisible par la machine des packages exacts dont vous dépendez), un répertoire de tests raccordé, et R CMD check pour détecter les problèmes automatiquement — la discipline qui garde une application de production honnête. La leçon sur le développement de packages traite cette étape ; la disposition — dossier R/ et convention de nommage — que vous venez de bâtir est exactement la structure qu’attend un package, donc le saut est petit.
  • Laissez golem l’automatiser. golem échafaude tout l’ensemble package-plus-structure en une commande et génère les modules, les fichiers auxiliaires, et même vos fichiers de déploiement à la demande. Il automatise tout ce qui est dans cette leçon et l’étape du package — adoptez-le quand vous livrez en production des applications maintenues et construites en équipe.

Les deux sont des étapes suivantes, pas celle-ci. Organisez-vous d’abord avec un dossier et une convention ; adoptez un package ou golem quand l’échelle de l’application l’exige vraiment.

Problèmes fréquents

Une fonction auxiliaire marche quand vous l’exécutez à la main, mais l’application dit qu’elle est introuvable. Vous avez défini summarise_mpg() dans la console (donc votre session la connaît) mais l’application, non — l’application en cours d’exécution ne connaît que ce qu’elle charge avec source() au démarrage. Assurez-vous que la fonction se trouve dans un fichier à l’intérieur de R/ et que votre boucle source() (for (f in list.files("R", full.names = TRUE)) source(f)) s’exécute bien avant que server ne l’utilise. Si la fonction est dans R/ et reste introuvable, vérifiez que la boucle pointe vers le bon dossier et s’exécute en haut de app.R ou dans global.R.

Vous vous attendiez à ce que global.R s’exécute pour chaque utilisateur, ou à ce que le code de server ne s’exécute qu’une fois. C’est l’inverse. global.R est évalué une seule fois au démarrage du processus de l’application et est partagé par toutes les sessions — placez-y la configuration partagée et coûteuse. La fonction server s’exécute une fois par session utilisateur — placez-y la logique réactive propre à chaque utilisateur. Charger un gros jeu de données dans server le relit pour chaque visiteur ; cela a sa place dans global.R.

Un fichier R/ a besoin d’une fonction qu’un autre fichier définit, et il échoue au chargement. Comme la boucle source() lit les fichiers par ordre alphabétique, un fichier situé tôt dans l’alphabet ne peut pas appeler une fonction définie dans un fichier plus loin au moment du source (du code de premier niveau qui s’exécute immédiatement). Cela pose rarement problème, car le corps des fonctions n’est pas évalué avant leur appel — moment auquel tous les fichiers sont déjà chargés. Si vous avez effectivement du code de premier niveau avec une dépendance entre fichiers, déplacez ce code dans une fonction, ou dans global.R où vous contrôlez l’ordre explicitement.

Questions fréquentes

Sortez le code de app.R vers un dossier R/ de fichiers plus petits et rechargez-les avec source(), avec un fichier par module ou par groupe de fonctions liées. Utilisez global.R pour la configuration ponctuelle au démarrage (packages, données, options), et adoptez une convention de nommage — mod_*.R pour les modules, fct_*.R pour la logique métier, utils_*.R pour les fonctions auxiliaires. Cela vous donne une base de code navigable et adaptée au travail en équipe, sans faire de l’application un package R (c’est une étape ultérieure optionnelle).

Le code qui s’exécute une seule fois au démarrage de l’application et qui est partagé à la fois par ui et server : les appels à library(), la lecture d’un jeu de données que vous réutiliserez au fil des sessions, et les options() valables pour toute l’application. Il est évalué une seule fois pour toute l’application, pas par utilisateur, donc la configuration partagée et coûteuse a sa place ici plutôt qu’à l’intérieur de server (qui se réexécute à chaque session). Beaucoup de gens y placent aussi la boucle source() qui charge R/.

Faites-le dès que chaque définition devient assez grande pour être malcommode dans un seul fichier — disons, une UI de plus de cent lignes. Placez-les dans R/app_ui.R et R/app_server.R (sous forme d’objets ou de fonctions), chargez-les avec source(), et laissez app.R se réduire à un mince lanceur qui appelle shinyApp(). Pour une petite application, c’est inutile ; un seul app.R est plus clair que trois fichiers. Séparez quand la taille gêne réellement, pas par principe.

global.R, c’est la configuration qui s’exécute une seule fois — charger les packages, lire les données partagées, définir les options. Le dossier R/, c’est le code de votre application — les fonctions et les modules qui font le travail, un rôle par fichier. Ils coopèrent : global.R (ou le haut de app.R) exécute une boucle source() qui charge tous les fichiers de R/ pour rendre leurs fonctions disponibles. Gardez la configuration ponctuelle dans global.R et la logique réutilisable dans R/.

Non. Un dossier R/ de fichiers chargés avec source(), plus global.R et une convention de nommage, vous apporte l’essentiel du bénéfice d’organisation sans aucune des lourdeurs d’un package. En faire un package ajoute le suivi des dépendances (DESCRIPTION), une suite de tests raccordée, et R CMD check — utile pour les applications maintenues, en équipe, en production, et l’étape suivante naturelle, mais pas requis juste pour s’organiser.

Testez vos connaissances

Voici une application en un seul fichier qui filtre mtcars pour ne garder que les voitures au-dessus d’un poids choisi et indique combien correspondent — la logique est en ligne dans server :

library(shiny)

ui <- fluidPage(
  sliderInput("wt", "Minimum weight", min = 1, max = 6, value = 3),
  textOutput("n")
)

server <- function(input, output, session) {
  output$n <- renderText({
    matched <- mtcars[mtcars$wt >= input$wt, ]
    paste(nrow(matched), "cars match")
  })
}

shinyApp(ui, server)

Refactorisez-la : déplacez le filtrage dans une fonction heavy_cars(data, min_wt) dans R/fct_filter.R, et réécrivez app.R pour charger le dossier R/ avec source() et appeler la fonction. L’application doit se comporter à l’identique.

Créez R/fct_filter.R avec une fonction qui prend les données et un seuil et renvoie les lignes filtrées à l’aide de l’indexation base-R (data[data$wt >= min_wt, ]). En haut de app.R, ajoutez la boucle for (f in list.files("R", full.names = TRUE)) source(f), puis appelez heavy_cars(mtcars, input$wt) là où se trouvait le filtre en ligne.

# R/fct_filter.R
heavy_cars <- function(data, min_wt) {
  data[data$wt >= min_wt, ]
}
# app.R
library(shiny)
for (f in list.files("R", full.names = TRUE)) source(f)

ui <- fluidPage(
  sliderInput("wt", "Minimum weight", min = 1, max = 6, value = 3),
  textOutput("n")
)

server <- function(input, output, session) {
  output$n <- renderText({
    matched <- heavy_cars(mtcars, input$wt)
    paste(nrow(matched), "cars match")
  })
}

shinyApp(ui, server)

La logique de filtrage se trouve maintenant dans R/fct_filter.R (un fichier fct_, puisque c’est de la logique métier), app.R le charge avec la boucle, et server appelle heavy_cars(). Déplacez le curseur et le décompte se met à jour exactement comme avant — vous avez réorganisé les fichiers sans toucher au comportement.

Vous avez une application qui lit un jeu de données de 200 Mo dont chaque utilisateur a besoin, et il est actuellement chargé à l’intérieur de la fonction server. Où le read devrait-il aller, et pourquoi ?

A. Laissez-le dans server — c’est là que va toute la logique de données.
B. Déplacez-le dans global.R — le code y est exécuté une seule fois au démarrage et partagé par toutes les sessions, de sorte que le fichier est lu une seule fois au lieu d’une fois par utilisateur.
C. Déplacez-le dans R/fct_data.R et ne l’appelez jamais — les fichiers de R/ se chargent automatiquement.
D. Peu importe ; Shiny met les lectures en cache automatiquement.

B. global.R est évalué une seule fois au démarrage du processus de l’application et ses objets sont partagés par chaque session, donc une grosse lecture partagée a sa place ici — chargée une seule fois et réutilisée. À l’intérieur de server (A), la lecture se réexécute pour chaque session utilisateur, gaspillant du temps et de la mémoire. C est faux, car une fonction dans R/ ne fait rien tant qu’elle n’est pas appelée — charger un fichier ne fait que définir ses fonctions. D est inventé : Shiny ne met pas automatiquement en cache vos lectures de fichiers.

Conclusion

Organiser une application Shiny est un geste petit et sûr : sortez la logique de app.R vers un dossier R/ de fichiers nommés, rechargez-les avec source(), placez la configuration ponctuelle dans global.R, et séparez le ui et le server quand ils deviennent gros. Une convention de nommage — mod_*.R, fct_*.R, utils_*.R, app_ui.R, app_server.R — transforme le dossier en une carte que tout le monde peut lire. Rien de tout cela ne change ce que fait l’application ; l’application découpée construit le même shiny.appobj et calcule les mêmes résultats, ce qui est justement la façon de confirmer que le refactoring est propre. Quand l’application grossit au point de vouloir un suivi des dépendances et des tests, faites-en un package ; quand vous voulez que tout soit échafaudé et automatisé, adoptez golem. Organisez-vous d’abord — le reste s’appuie là-dessus.

Leçons connexes

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

Prouvez que vous savez le faire. Maîtrisez toute la série Architecture Shiny — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

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

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

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 = {Organisation du code Shiny : structurer un app.R en fichiers
    R/},
  date = {2026-07-16},
  url = {https://www.datanovia.com/learn/programming/shiny/architecture/code-organization},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Organisation du code Shiny : structurer un app.R en fichiers R/.” 2026. July 16. https://www.datanovia.com/learn/programming/shiny/architecture/code-organization.