Intégration JavaScript dans Shiny : le pont bidirectionnel R <-> JS

Envoyez des données de R vers le navigateur et en retour avec sendCustomMessage et setInputValue, et chargez votre propre fichier .js de façon portable — les cas limites du JavaScript personnalisé que les widgets intégrés ne peuvent pas atteindre.

Programming
Shiny

Câblez du JavaScript personnalisé dans une application Shiny avec un pont bidirectionnel R vers JS. Envoyez des données de R vers le navigateur avec sendCustomMessage() et Shiny.addCustomMessageHandler(), renvoyez-les avec Shiny.setInputValue(), et chargez un fichier .js avec addResourcePath() et une balise script. Un aller-retour exécutable, plus une brève note sur les bibliothèques tierces et les liaisons d’entrée personnalisées.

Date de publication

16 juillet 2026

Modifié

16 juillet 2026

AstucePoints clés
  • Shiny vous offre un pont bidirectionnel vers le navigateur. R envoie des données vers JavaScript avec session$sendCustomMessage() ; JavaScript renvoie des données vers R avec Shiny.setInputValue(). Entre les deux, vous pouvez atteindre n’importe quelle fonctionnalité du navigateur que R ne peut pas toucher.
  • R → JS, c’est un message nommé + un gestionnaire. session$sendCustomMessage("updateText", list(value = input$n)) dans le serveur ; Shiny.addCustomMessageHandler("updateText", function(message){ ... }) dans le navigateur le lit comme message.value.
  • JS → R, c’est setInputValue vers input$. Shiny.setInputValue("js_click", value, {priority: "event"}) fait se déclencher input$js_click dans votre serveur. {priority: "event"} est ce qui fait fonctionner un bouton — il se déclenche même lorsque la valeur se répète.
  • Chargez votre fichier .js avec addResourcePath(). addResourcePath("assets", "www") + tags$head(tags$script(src = "assets/custom.js")) sert et charge le fichier de façon fiable ; pour les extraits d’une ligne, insérez-le en ligne avec tags$script(HTML(...)).
  • The runtime is the judge. Tout cet aller-retour s’exécute dans le navigateur : cliquer sur un bouton a exécuté la ligne du serveur et JavaScript a affiché « JS received: 3 » ; un simple bouton JS a défini input$js_click et R l’a réaffiché. Copiez l’application et regardez-le se produire.

Introduction

Vous avez atteint les limites des widgets intégrés de Shiny. Vous voulez un raccourci clavier, une animation fluide, une API de navigateur comme la géolocalisation ou le presse-papiers, ou une bibliothèque JavaScript de graphiques particulière — et rien de tout cela n’est un sliderInput() ou un renderPlot(). Le réflexe est d’abandonner Shiny pour un framework frontal complet. Ce n’est pas nécessaire. Shiny maintient un canal ouvert entre R et le navigateur, et il expose ce canal comme un pont bidirectionnel propre que vous pouvez piloter depuis votre propre JavaScript.

Cette leçon enseigne les deux primitives sur lesquelles ce pont repose — envoyer des données de R vers le navigateur, et les renvoyer du navigateur vers R — plus la façon portable de charger un fichier .js. C’est le cœur qui porte la demande ; tout ce qui est plus sophistiqué (une bibliothèque tierce, un widget personnalisé réutilisable) n’est que ces deux mêmes mouvements dans un costume plus élaboré.

Quand ne pas recourir au JavaScript personnalisé. La plupart des applications n’en ont jamais besoin. Si un contrôle d’entrée intégré, une sortie intégrée ou un htmlwidget existant (plotly, leaflet, DT) fait déjà l’affaire, utilisez-le — il est testé, accessible et réactif d’emblée. Le JavaScript personnalisé est réservé au véritable manque : une capacité du navigateur sans surcouche R. Y recourir alors qu’un widget existe est une sur-ingénierie que vous maintiendrez pour toujours.

NotePrérequis

Vous aurez besoin d’une application Shiny fonctionnelle à câbler, d’un peu de JavaScript (assez pour lire une fonction et un gestionnaire d’événement) et de savoir où vivent les fichiers statiques d’une application — le dossier www/, que Shiny sert au navigateur. Où placer le code de démarrage et les ressources statiques est traité dans l’organisation du code Shiny, sur laquelle cette leçon s’appuie. La réactivité et les entrées intégrées — reactive(), observeEvent(), sliderInput() — relèvent de la série interactivité ; cette leçon les suppose plutôt que de les réenseigner.

NoteCopiez n’importe quel bloc et exécutez-le en local

L’aller-retour R ↔︎ JS ne se produit que dans un navigateur en direct qui dialogue avec un serveur R en cours d’exécution — une page web statique ne peut pas ouvrir ce canal. Rien n’est donc exécuté sur la page ici. Copiez n’importe quel bloc sur votre machine et exécutez-le localement : enregistrez l’application sous app.R (et, là où c’est indiqué, un www/custom.js à côté), lancez-la et cliquez sur les boutons. Lorsque la page passe à « JS received: 3 » et que votre console R affiche la valeur renvoyée par le navigateur, vous avez prouvé le pont de bout en bout. The runtime is the judge.

Le modèle mental : un canal, deux directions

Shiny détient déjà une connexion en direct entre le serveur R et le navigateur — le même canal que celui qu’empruntent vos entrées et vos sorties. Le JavaScript personnalisé emprunte ce canal dans deux directions, et chaque direction est un appel de fonction de chaque côté :

Direction Dans R (le serveur) Dans JavaScript (le navigateur)
R → navigateur session$sendCustomMessage("<name>", <payload>) Shiny.addCustomMessageHandler("<name>", function(message){ ... })
navigateur → R lisez-la comme input$<name> Shiny.setInputValue("<name>", value, {priority: "event"})

Un gestionnaire de message personnalisé est une fonction JavaScript que vous enregistrez sous un nom ; lorsque R envoie un message portant ce nom, Shiny appelle votre fonction avec la charge utile. Dans l’autre sens, setInputValue écrit directement dans l’objet input de R, de sorte qu’un clic dans le navigateur devient une valeur réactive que votre serveur peut observer. Maîtrisez ces deux-là et vous tenez tout le pont. Le guide Shiny « communicating with JavaScript » est la référence principale pour les deux.

R → JS : envoyer des données au navigateur

Supposons qu’un curseur soit sur 3 et que vous vouliez que le navigateur fasse quelque chose de ce nombre — l’animer, l’injecter dans un graphique, faire clignoter une bannière. Dans le serveur, appelez session$sendCustomMessage() avec un nom pour le message et une charge utile (n’importe quelle liste R, qui arrive dans JavaScript sous forme d’objet) :

# app.R
library(shiny)

ui <- fluidPage(
  tags$head(
    tags$script(HTML("
      Shiny.addCustomMessageHandler('updateText', function(message) {
        document.getElementById('out').innerText = 'JS received: ' + message.value;
      });
    "))
  ),
  sliderInput("n", "Pick a number", min = 1, max = 10, value = 3),
  actionButton("send", "Send to the browser"),
  tags$p(id = "out", "Waiting for a message...")
)

server <- function(input, output, session) {
  observeEvent(input$send, {
    session$sendCustomMessage("updateText", list(value = input$n))
  })
}

shinyApp(ui, server)

Lisez-le en deux moitiés. La moitié serveur est observeEvent(input$send, ...) : lorsque le bouton est cliqué, sendCustomMessage("updateText", list(value = input$n)) pousse un message nommé "updateText" transportant la valeur du curseur à travers le canal. La moitié navigateur est Shiny.addCustomMessageHandler("updateText", ...) : elle enregistre une fonction sous le même nom, et Shiny l’appelle avec la charge utile comme message. Comme la liste R était list(value = ...), l’objet JavaScript possède un champ .value, donc message.value est le nombre — et document.getElementById('out').innerText = ... l’écrit dans la page.

document.getElementById(...) accède au DOM (Document Object Model — l’arbre vivant, en mémoire, des éléments de la page que tient le navigateur) pour attraper le <p id="out"> et changer son texte. Exécutez ceci, laissez le curseur sur 3, cliquez sur Send to the browser, et le paragraphe devient « JS received: 3 ». Le nom du message est le contrat : il doit correspondre exactement des deux côtés.

JS → R : renvoyer des données au serveur

L’autre direction permet à un événement du navigateur de devenir une valeur réactive R. Shiny.setInputValue("js_click", value) écrit value dans input$js_click sur le serveur — ainsi n’importe quel JavaScript que vous écrivez (un clic, une frappe de touche, un résultat d’API de navigateur) peut piloter votre code réactif :

# app.R
library(shiny)

ui <- fluidPage(
  tags$head(
    tags$script(HTML("
      document.addEventListener('DOMContentLoaded', function() {
        document.getElementById('js_btn').addEventListener('click', function() {
          Shiny.setInputValue('js_click', Date.now(), {priority: 'event'});
        });
      });
    "))
  ),
  tags$button(id = "js_btn", class = "btn btn-primary", "Click me (JS)"),
  textOutput("from_js")
)

server <- function(input, output, session) {
  output$from_js <- renderText({
    req(input$js_click)
    paste("R received js_click =", input$js_click)
  })
}

shinyApp(ui, server)

Le JavaScript attache un gestionnaire de clic au bouton et, à chaque clic, appelle setInputValue("js_click", Date.now(), {priority: "event"}). Sur le serveur, input$js_click est désormais une entrée réactive normale : renderText() la lit et met à jour la sortie. Cliquez sur le bouton et R affiche quelque chose comme « R received js_click = 1784185171 » (Date.now() est l’horodatage courant, donc le nombre exact change à chaque clic).

Pourquoi {priority: "event"} compte — et compte particulièrement pour les boutons. Sans cette option, setInputValue se comporte comme une entrée ordinaire : il ne se déclenche que lorsque la valeur change. Envoyez deux fois la même valeur et le second envoi est sans effet — rien ne réagit. Un bouton qui signale « cliqué » fonctionnerait la première fois puis resterait inerte à chaque nouveau clic. {priority: "event"} force Shiny à déclencher l’entrée à chaque fois, même lorsque la valeur est identique, ce qui est exactement ce dont une action répétable a besoin. Utilisez-le dès que c’est l’événement qui compte, pas seulement la valeur.

Nous avons enveloppé le gestionnaire dans DOMContentLoaded — un événement du navigateur qui se déclenche une fois que le HTML de la page est entièrement analysé (voir la référence MDN) — afin que l’élément bouton existe avant que nous essayions de nous y attacher. $(document).on("shiny:connected", ...) est l’alternative propre à Shiny lorsque votre code a besoin que Shiny lui-même soit prêt.

Charger votre JavaScript depuis un fichier

Insérer le JavaScript en ligne avec tags$script(HTML(...)) convient pour quelques lignes, mais une vraie logique a sa place dans son propre fichier. Placez-le dans le dossier www/ de votre application et chargez-le — la façon fiable et portable consiste à exposer d’abord ce dossier avec addResourcePath() :

# app.R
library(shiny)

addResourcePath("assets", "www")   # serve the www/ folder at the /assets/ URL prefix

ui <- fluidPage(
  tags$head(tags$script(src = "assets/custom.js")),
  sliderInput("n", "Pick a number", min = 1, max = 10, value = 3),
  actionButton("send", "Send to the browser"),
  tags$p(id = "out", "Waiting for a message...")
)

server <- function(input, output, session) {
  observeEvent(input$send, {
    session$sendCustomMessage("updateText", list(value = input$n))
  })
}

shinyApp(ui, server)
// www/custom.js
Shiny.addCustomMessageHandler("updateText", function(message) {
  document.getElementById("out").innerText = "JS received: " + message.value;
});

addResourcePath("assets", "www") indique à Shiny de servir le répertoire local www/ sous le préfixe d’URL /assets/ (sa page de référence documente cette correspondance). Ensuite, tags$script(src = "assets/custom.js") charge le fichier depuis cette URL, et le gestionnaire qu’il contient s’exécute exactement comme le faisait la version en ligne — même résultat « JS received: 3 ».

Soyons honnêtes sur le piège. Shiny est documenté comme servant automatiquement www/ à la racine de l’application, donc en principe src = "custom.js" devrait fonctionner sans addResourcePath(). En pratique, cette diffusion automatique est peu fiable lorsque vous lancez l’objet application directement — exécuter l’application construite par shinyApp(ui, server) (plutôt que depuis un app.R voisin d’un www/ via runApp()) peut laisser src = "custom.js" renvoyer une erreur 404 et votre gestionnaire ne jamais se charger. addResourcePath() rend le chemin explicite et fonctionne dans les deux cas, alors préférez-le. Si vous n’ajoutez qu’une poignée de lignes, sautez complètement le fichier et insérez-les en ligne avec tags$head(tags$script(HTML("..."))) comme le faisaient les exemples précédents.

N’incluez qu’une seule fois les balises d’en-tête partagées. Si le même <script> pouvait être injecté plusieurs fois — par exemple depuis un module Shiny utilisé plusieurs fois sur une page — enveloppez-le dans singleton() pour que Shiny ne l’émette qu’une seule fois :

tags$head(
  singleton(tags$script(src = "assets/custom.js"))
)

Bibliothèques tierces et liaisons d’entrée personnalisées

Une bibliothèque JavaScript tierce est le même schéma, à plus grande échelle. Chargez le JS de la bibliothèque (et son éventuel CSS) dans l’en-tête — depuis une URL de CDN ou un fichier local exposé par addResourcePath() — puis pilotez-la depuis R avec sendCustomMessage() et rapportez les actions de l’utilisateur avec setInputValue() :

# app.R (sketch)
ui <- fluidPage(
  tags$head(
    tags$script(src = "https://cdn.example.com/some-lib.min.js"),
    tags$link(rel = "stylesheet", href = "https://cdn.example.com/some-lib.css")
  ),
  # ... your UI ...
)

À partir de là, un addCustomMessageHandler alimente la bibliothèque en données depuis R, et un setInputValue placé dans les propres rappels de la bibliothèque renvoie les clics ou les sélections. Le mécanisme, ce sont exactement les deux primitives ci-dessus — il n’y a pas de troisième API à apprendre.

Pour un widget personnalisé véritablement réutilisable — une entrée que vous déposerez dans de nombreuses applications, que Shiny devrait traiter comme un contrôle de première classe — l’outil plus complet est une liaison d’entrée personnalisée : une sous-classe de Shiny.InputBinding enregistrée avec Shiny.inputBindings.register(...), qui indique à Shiny comment trouver votre élément, lire sa valeur et écouter les changements. C’est le grand bain, et la plupart des besoins JavaScript-vers-R ne l’exigent jamais — Shiny.setInputValue() couvre la grande majorité. Lorsque vous en avez besoin, suivez le guide Shiny des entrées personnalisées.

Problèmes fréquents

Votre gestionnaire ne s’exécute jamais — le fichier .js renvoie une 404. Vous avez écrit www/custom.js et tags$script(src = "custom.js"), mais le message ne fait rien et la console du navigateur affiche une 404 pour le fichier. La diffusion automatique implicite de www/ ne s’est pas déclenchée (fréquent lorsque vous lancez l’objet application directement). Correctif : exposez explicitement le dossier avec addResourcePath("assets", "www") et chargez depuis ce préfixe — tags$script(src = "assets/custom.js"). Vérifiez le chemin exact dans l’onglet Réseau de votre navigateur.

setInputValue se déclenche une fois, puis plus jamais. Un bouton JS signale son premier clic puis reste muet à chaque répétition. Sans {priority: "event"}, setInputValue ne réagit que lorsque la valeur change, donc envoyer deux fois la même valeur est ignoré. Ajoutez l’option — Shiny.setInputValue("js_click", value, {priority: "event"}) — pour que l’entrée se déclenche à chaque événement, quelle que soit la valeur.

Votre JavaScript s’exécute avant que la page (ou Shiny) soit prête. Du code qui appelle document.getElementById(...) en tête d’un script peut s’exécuter avant que cet élément existe, si bien qu’il trouve null et échoue silencieusement. Enveloppez le code qui touche aux éléments dans document.addEventListener("DOMContentLoaded", ...), et le code qui a besoin de Shiny lui-même dans $(document).on("shiny:connected", ...), pour qu’il ne s’exécute qu’une fois le DOM (et la connexion Shiny) en place.

Questions fréquentes

Placez votre code dans un fichier .js dans le dossier www/ de l’application, exposez le dossier avec addResourcePath("assets", "www"), et chargez-le dans l’UI avec tags$head(tags$script(src = "assets/custom.js")). Pour un court extrait, vous pouvez sauter le fichier et l’insérer en ligne : tags$head(tags$script(HTML("..."))). Dans votre JavaScript, utilisez Shiny.addCustomMessageHandler() pour recevoir des données de R et Shiny.setInputValue() pour renvoyer des données.

Appelez session$sendCustomMessage("<name>", <payload>) dans votre serveur — la charge utile est n’importe quelle liste R, qui arrive dans le navigateur comme un objet JavaScript. Dans le navigateur, enregistrez un gestionnaire correspondant : Shiny.addCustomMessageHandler("<name>", function(message) { ... }), et lisez les champs comme message.<field>. Le nom du message doit être identique des deux côtés. Voir le guide Shiny communicating-with-JS.

Shiny.setInputValue("id", value) s’exécute dans le navigateur et écrit dans le input$id du serveur, de sorte qu’un événement JavaScript peut déclencher votre code réactif. Un reactiveVal() vit entièrement dans R — c’est un état côté serveur que vous définissez avec du code R. Utilisez setInputValue pour faire entrer des événements du navigateur dans R ; utilisez un reactiveVal pour conserver des valeurs que vous calculez dans R. Pour un bouton, ajoutez {priority: "event"} à setInputValue afin qu’il se déclenche même lorsque la valeur se répète.

Parce que Shiny.setInputValue() se comporte par défaut comme une entrée normale : il ne se déclenche que lorsque la valeur change, donc cliquer sur un bouton qui envoie toujours la même valeur est sans effet après la première fois. Passez {priority: "event"} comme troisième argument — Shiny.setInputValue("js_click", value, {priority: "event"}) — et Shiny déclenche l’entrée à chaque appel, ce dont une action répétable a besoin.

Vous le pouvez, mais cela en vaut rarement la peine. Un framework moderne apporte une chaîne d’outils de build (npm, un bundler) et son propre modèle d’état, qui tous deux s’opposent au système réactif de Shiny et compliquent le déploiement. La voie plus légère et mieux intégrée pour la plupart des besoins est du JavaScript simple par-dessus le pont de cette leçon — sendCustomMessage et setInputValue — ou, pour un composant de framework que vous voulez réutiliser, l’envelopper en htmlwidget. Ne recourez à un framework complet que lorsque l’interface frontale est réellement vaste et proche d’une application.

Testez vos connaissances

Construisez une seule application qui utilise le pont dans les deux directions. (1) Un bouton HTML simple qui, une fois cliqué, envoie l’heure courante à R avec Shiny.setInputValue() — configuré pour se déclencher à chaque clic. (2) Lorsque R le reçoit, le serveur renvoie un message vers le navigateur, et JavaScript affiche ce message dans un <p id="status">. Esquissez le app.R et le JavaScript.

La moitié JS → R est Shiny.setInputValue("clicked", Date.now(), {priority: "event"}) sur le gestionnaire de clic du bouton. Sur le serveur, observeEvent(input$clicked, { session$sendCustomMessage("showStatus", list(text = ...)) }) envoie R → JS. Dans le navigateur, Shiny.addCustomMessageHandler("showStatus", function(message){ ... message.text ... }) écrit dans le paragraphe. Enveloppez le gestionnaire de clic du bouton dans DOMContentLoaded pour que l’élément existe d’abord.

# app.R
library(shiny)

ui <- fluidPage(
  tags$head(
    tags$script(HTML("
      document.addEventListener('DOMContentLoaded', function() {
        document.getElementById('go').addEventListener('click', function() {
          Shiny.setInputValue('clicked', Date.now(), {priority: 'event'});
        });
      });
      Shiny.addCustomMessageHandler('showStatus', function(message) {
        document.getElementById('status').innerText = message.text;
      });
    "))
  ),
  tags$button(id = "go", class = "btn btn-primary", "Ping R"),
  tags$p(id = "status", "No pings yet.")
)

server <- function(input, output, session) {
  observeEvent(input$clicked, {
    session$sendCustomMessage(
      "showStatus",
      list(text = paste("R got your click at", input$clicked))
    )
  })
}

shinyApp(ui, server)

Le clic appelle setInputValue avec {priority: "event"} afin qu’il se déclenche à chaque pression ; observeEvent(input$clicked, ...) réagit sur le serveur et sendCustomMessage("showStatus", ...) renvoie un message ; le gestionnaire showStatus écrit message.text dans le paragraphe. C’est un aller-retour complet — du navigateur vers R et retour — sur un seul bouton.

Quelle fonction envoie des données de R vers le navigateur ?

A. Shiny.setInputValue()
B. session$sendCustomMessage()
C. addResourcePath()
D. observeEvent()

B. session$sendCustomMessage("<name>", <payload>) s’exécute dans le serveur R et pousse un message nommé vers le navigateur, où un Shiny.addCustomMessageHandler() correspondant le reçoit. A va dans l’autre sens — Shiny.setInputValue() s’exécute dans le navigateur et écrit dans input$ sur le serveur. C expose un dossier de fichiers statiques (comme votre .js) à un préfixe d’URL ; il ne transmet pas de données. D est un observateur réactif côté serveur, pas un message vers le navigateur.

Conclusion

Le JavaScript personnalisé dans Shiny se résume à un canal et deux appels. session$sendCustomMessage() dans R, associé à Shiny.addCustomMessageHandler() dans le navigateur, pousse des données vers JavaScript ; Shiny.setInputValue() dans le navigateur, lu comme input$ dans R, ramène les événements du navigateur en retour. Ajoutez {priority: "event"} lorsque l’événement se répète (boutons), et chargez votre fichier .js avec addResourcePath() + tags$script(src = ...) — en n’insérant en ligne que les plus petits extraits. Les bibliothèques tierces et les liaisons d’entrée personnalisées sont ces deux mêmes primitives avec plus d’échafaudage, alors n’y recourez que lorsqu’un widget intégré ou un htmlwidget ne peut réellement pas faire le travail. Gardez le pont petit et l’application reste maintenable — vous obtenez toute la portée du navigateur sans quitter R.

Leçons connexes

  • Bâtir sur les pièces qui l’entourent : la série interactivité couvre la réactivité, les entrées et les sorties intégrées que cette leçon suppose plutôt que de les réenseigner ; l’organisation du code Shiny montre où placer le code de démarrage (comme addResourcePath()) et vos ressources www/ ; et les modules Shiny sont là où singleton() prend toute sa valeur, en empêchant un <script> partagé d’être injecté une fois par instance de module.
  • Aller plus loin : Applications web Shiny — tout le cursus Shiny. · Science computationnelle et IA — le pilier.
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 = {Intégration JavaScript dans Shiny : le pont bidirectionnel R
    \textless-\textgreater{} JS},
  date = {2026-07-16},
  url = {https://www.datanovia.com/learn/programming/shiny/architecture/javascript-integration},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Intégration JavaScript dans Shiny : le pont bidirectionnel R <-> JS.” 2026. July 16. https://www.datanovia.com/learn/programming/shiny/architecture/javascript-integration.