Mises à jour en temps réel Shiny : invalidateLater, reactiveTimer & reactivePoll
Faites s’actualiser une application Shiny d’elle-même — invalidez un réactif selon une planification avec invalidateLater, partagez un tic avec reactiveTimer, et interrogez à peu de frais une base de données ou un fichier changeant.
La réactivité se déclenche normalement sur une entrée utilisateur. Pour une actualisation pilotée par le temps, vous invalidez un réactif selon une planification. Cette leçon couvre invalidateLater() pour un tic ad hoc (une horloge ou un compteur auto-actualisé), reactiveTimer() pour un minuteur partagé dont plusieurs sorties dépendent, reactivePoll() pour interroger une source changeante comme une base de données avec une vérification peu coûteuse puis une récupération, et reactiveFileReader() pour surveiller un fichier — plus comment mettre en pause, reprendre, et garder l’intervalle raisonnable pour ne pas surcharger le server.
- La réactivité se déclenche sur une entrée ; un minuteur se déclenche sur l’horloge. Pour actualiser sans action de l’utilisateur, vous invalidez un réactif selon une planification — il se réexécute comme si une entrée avait changé.
invalidateLater(millis)est le tic ad hoc. Appelé à l’intérieur d’unreactive()ou d’unobserve(), il planifie la réexécution de ce bloc aprèsmillismillisecondes — une horloge ou un compteur auto-actualisé en une ligne.reactiveTimer(intervalMs)est un tic partagé. Créez-le une fois, puis appelez-le (autoInvalidate()) partout où vous voulez le même battement — plusieurs sorties peuvent dépendre d’un seul minuteur.reactivePoll()surveille une source changeante à peu de frais. UncheckFuncpeu coûteux détecte si les données ont changé ; levalueFuncplus lourd ne les récupère que lorsque c’est le cas — l’outil idéal pour une base de données ou une API.reactiveFileReader()est la spécialisation fichier. Même idée, câblée à un chemin de fichier : il relit le fichier seulement quand sa date de modification change.- Maîtrisez le coût. Mettez en pause et reprenez avec un drapeau
actionButton()+reactiveVal()(oureq()), et gardez l’intervalle raisonnable — une interrogation à 100 ms surcharge le server.
Introduction
Chaque application Shiny que vous avez construite jusqu’ici réagit à l’utilisateur : déplacez un curseur, cliquez sur un bouton, téléversez un fichier, et les sorties se recalculent. Mais certaines applications doivent s’actualiser sans que personne ne touche à rien — une horloge qui tourne, un tableau de bord qui récupère des chiffres frais toutes les quelques secondes, un moniteur qui surveille un fichier journal. Les données changent d’elles-mêmes, et l’application doit suivre.
La réponse de Shiny est d’invalider un réactif selon une planification. La réactivité réexécute déjà un bloc quand l’une de ses dépendances change ; un minuteur n’est qu’une dépendance qui « change » au rythme de l’horloge. Cette leçon couvre les quatre outils qui pilotent l’actualisation temporelle — invalidateLater() pour un tic ad hoc, reactiveTimer() pour un tic partagé, reactivePoll() pour une source de données changeante, et reactiveFileReader() pour un fichier — et comment empêcher l’actualisation de submerger votre server.
Le code de cette page n’est pas exécuté ici — une application Shiny a besoin d’une session R active, 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 fonctions intégrées comme Sys.time() et rnorm(), ils tournent donc tels quels.
L’idée centrale : invalider selon une planification
Un réactif se réexécute quand quelque chose dont il dépend change. input$bins change, et chaque sortie qui le lit se recalcule. L’actualisation temporelle fonctionne de la même façon — vous ajoutez une dépendance qui devient invalide après un certain délai, de sorte que le bloc se réexécute au rythme de l’horloge plutôt que sur un clic.
Cette dépendance, c’est ce que créent invalidateLater() et reactiveTimer(). Rien d’autre dans la réactivité ne change : vous écrivez toujours un reactive() ou un observe(), vous lisez toujours des entrées, vous rendez toujours des sorties. Vous donnez simplement au bloc une raison de se réexécuter alors qu’aucune entrée ne l’a fait.
invalidateLater() : le tic ad hoc
invalidateLater(millis) est l’actualisation automatique la plus simple. Appelez-le à l’intérieur d’un reactive() ou d’un observe() et il planifie la réexécution de ce même bloc après millis millisecondes. Chaque réexécution rappelle invalidateLater(), si bien qu’il continue de tourner — une boucle qui se relance d’elle-même.
La démo classique est une horloge qui se met à jour toutes les secondes :
library(shiny)
ui <- fluidPage(
h2("Live clock"),
textOutput("now")
)
server <- function(input, output, session) {
output$now <- renderText({
invalidateLater(1000, session) # re-run this block in 1000 ms (1 second)
format(Sys.time(), "%H:%M:%S") # read the clock again each tick
})
}
shinyApp(ui, server)invalidateLater(1000, session) planifie l’invalidation du bloc renderText() une seconde plus tard ; quand cela arrive, le bloc se réexécute, relit Sys.time(), et rappelle invalidateLater() pour relancer le cycle. Passez session en deuxième argument pour que le minuteur s’arrête quand l’utilisateur ferme l’onglet du navigateur — sans lui, la planification peut survivre à la session.
Cette même ligne pilote un compteur ou un flux simulé. Conservez l’état dans un reactiveVal() et incrémentez-le à l’intérieur d’un observe() :
server <- function(input, output, session) {
tick <- reactiveVal(0)
observe({
invalidateLater(1000, session) # fire every second
tick(isolate(tick()) + 1) # increment the counter on each tick
})
output$count <- renderText({
paste("Ticks:", tick())
})
}invalidateLater() réexécute l’observe() chaque seconde, et chaque exécution ajoute un à tick. Lire tick() à l’intérieur du même observateur qui l’écrit créerait une boucle de rétroaction, alors enveloppez la lecture dans isolate() — il prend la valeur courante sans rendre l’observateur dépendant d’elle. La sortie lit ensuite tick() normalement et se met à jour à chaque changement du compteur.
invalidateLater() ne fonctionne qu’à l’intérieur d’un contexte réactif
invalidateLater() planifie la réexécution du bloc réactif courant. Appelé en haut de la fonction server — en dehors de tout reactive(), observe() ou render*() — il n’y a aucun bloc à planifier, donc il ne fait silencieusement rien. Il doit vivre à l’intérieur de l’un de ces contextes réactifs.
reactiveTimer() : un tic partagé
invalidateLater() donne à chaque bloc sa propre planification privée. Quand plusieurs sorties doivent s’actualiser sur le même battement, créez un seul reactiveTimer() et faites-les toutes en dépendre. Vous définissez le minuteur une fois, puis appelez-le comme une fonction partout où vous voulez cette dépendance.
server <- function(input, output, session) {
# one timer, fires every 2 seconds
autoInvalidate <- reactiveTimer(2000)
output$plot <- renderPlot({
autoInvalidate() # take a dependency on the shared tick
hist(rnorm(100), col = "#3a86d4", main = "Fresh sample")
})
output$stamp <- renderText({
autoInvalidate() # same tick drives this output too
format(Sys.time(), "%H:%M:%S")
})
}reactiveTimer(2000) renvoie une fonction ; l’appeler à l’intérieur d’un bloc réactif (autoInvalidate()) enregistre ce bloc comme une dépendance, de sorte qu’il se réexécute toutes les deux secondes. Le graphique et l’horodatage appellent tous deux le même minuteur, ils s’actualisent donc ensemble sur une seule planification — il y a un battement unique au lieu de deux qui dérivent. Recourez à reactiveTimer() quand plusieurs sorties partagent une horloge ; recourez à invalidateLater() quand un bloc a besoin de sa propre planification ad hoc.
reactivePoll() : surveiller une source de données changeante
Un minuteur qui relit une base de données chaque seconde fonctionne, mais c’est du gaspillage — la plupart des tics récupèrent des données qui n’ont pas changé. reactivePoll() divise le travail en deux pour que vous ne payiez le coût complet que lorsqu’il y a du nouveau :
- un
checkFuncpeu coûteux qui s’exécute à chaque tic et renvoie une petite valeur (un nombre de lignes, un horodatage, un hachage) — quelque chose qui change quand les données changent ; - un
valueFuncplus lourd qui récupère effectivement les données, exécuté seulement quand la valeur de retour decheckFuncchange par rapport au tic précédent.
server <- function(input, output, session) {
latest <- reactivePoll(
intervalMillis = 5000, # check every 5 seconds
session = session,
# CHEAP: does the data look different? (a fast COUNT, not the whole table)
checkFunc = function() {
DBI::dbGetQuery(con, "SELECT COUNT(*) FROM events")[[1]]
},
# FETCH: only runs when checkFunc's value changed
valueFunc = function() {
DBI::dbGetQuery(con, "SELECT * FROM events ORDER BY ts DESC LIMIT 100")
}
)
output$tbl <- renderTable({ head(latest()) })
}Toutes les 5 secondes, Shiny exécute checkFunc — un unique COUNT(*) rapide. Si le compte est le même que la dernière fois, rien d’autre ne se produit. C’est seulement quand il change que Shiny exécute le valueFunc coûteux pour tirer les lignes. latest() se comporte alors comme n’importe quel réactif : lisez-le depuis les sorties et elles se mettent à jour quand des données fraîches arrivent. Tout le gain repose sur le fait que checkFunc soit peu coûteux — si votre vérification est aussi coûteuse que la récupération, vous n’avez rien gagné par rapport à un simple minuteur.
reactiveFileReader() : surveiller un fichier
Interroger un fichier est si courant que Shiny fournit une spécialisation. reactiveFileReader() est un reactivePoll() dont la vérification est précâblée sur la date de modification du fichier : il relit le fichier seulement quand cet horodatage change.
server <- function(input, output, session) {
log_data <- reactiveFileReader(
intervalMillis = 2000, # check the file every 2 seconds
session = session,
filePath = "data/live_log.csv",
readFunc = read.csv # how to read it when it changed
)
output$tail <- renderTable({ tail(log_data(), 10) })
}Vous lui donnez l’intervalle, le filePath et un readFunc (ici read.csv). Il vérifie la date de modification du fichier à chaque tic et appelle readFunc(filePath) seulement quand le fichier a réellement changé — ainsi un journal écrit une fois par minute est lu une fois par minute, et non toutes les deux secondes. Utilisez-le chaque fois que vos données « en direct » sont un fichier écrit par un autre processus.
Maîtriser le coût
Une application qui s’actualise automatiquement tourne indéfiniment et consomme le server pendant ce temps, donc deux habitudes importent.
Laissez l’utilisateur mettre en pause et reprendre. Conditionnez l’actualisation à un drapeau que l’utilisateur contrôle — un reactiveVal() basculé par un actionButton(). Quand le drapeau est désactivé, req() arrête le bloc avant qu’il ne planifie le tic suivant :
ui <- fluidPage(
actionButton("toggle", "Pause / resume"),
textOutput("now")
)
server <- function(input, output, session) {
running <- reactiveVal(TRUE)
observeEvent(input$toggle, { running(!running()) }) # flip on each click
output$now <- renderText({
req(running()) # if paused, stop here — no reschedule
invalidateLater(1000, session)
format(Sys.time(), "%H:%M:%S")
})
}Quand running() vaut FALSE, req(running()) arrête le bloc avant que invalidateLater() ne s’exécute, de sorte que le minuteur ne se replanifie pas et que l’horloge se fige. Cliquez de nouveau, running() devient TRUE, et le prochain rendu relance le tic.
Gardez l’intervalle raisonnable. Chaque tic coûte un aller-retour serveur et un recalcul. Un intervalle de 1 à 5 secondes est amplement suffisant pour un tableau de bord ou un moniteur ; une interrogation à 100 ms se déclenche dix fois par seconde et par utilisateur et submergera le server sous toute charge réelle. Ajustez l’intervalle à la vitesse réelle de changement des données — inutile d’interroger un flux qui change une fois par minute toutes les secondes.
Quel outil et quand
Tous les quatre pilotent l’actualisation temporelle ; ils diffèrent par leur portée et par ce qu’ils surveillent.
| Outil | Recourez-y quand |
|---|---|
invalidateLater(millis) |
Un bloc a besoin de son propre tic ad hoc — une horloge, un compteur, un flux simulé. |
reactiveTimer(intervalMs) |
Plusieurs sorties doivent s’actualiser sur le même battement partagé. |
reactivePoll(intervalMs, ...) |
La source est une base de données ou une API changeante — vérifiez à peu de frais, récupérez seulement au changement. |
reactiveFileReader(intervalMs, ...) |
La source est un fichier écrit par un autre processus — relisez seulement quand il change. |
Commencez par invalidateLater() pour tout ce qui est auto-généré, passez à reactiveTimer() quand des sorties doivent partager une horloge, et basculez vers reactivePoll()/reactiveFileReader() dès que les données vivent en dehors de l’application — ils vous évitent de re-récupérer des données inchangées à chaque tic.
Problèmes fréquents
invalidateLater() ne fait rien. Il planifie la réexécution du bloc réactif courant, il ne fonctionne donc qu’à l’intérieur d’un reactive(), d’un observe() ou d’un render*(). Appelé en haut de la fonction server — en dehors de tout contexte réactif — il n’y a aucun bloc à replanifier et rien ne se passe. Déplacez-le à l’intérieur du réactif qui doit s’actualiser, et passez session pour qu’il s’arrête à la fermeture de l’onglet.
Un intervalle trop court surcharge le server. invalidateLater(100) se réexécute dix fois par seconde, par utilisateur connecté — multipliez cela par un graphique lourd ou une requête de base de données et l’application rame ou s’effondre. Utilisez un intervalle qui correspond à la fréquence réelle de changement des données (1 à 5 secondes pour la plupart des tableaux de bord), et n’interrogez jamais plus vite que nécessaire.
reactivePoll() exécute la requête complète à chaque tic. Tout l’intérêt est un checkFunc peu coûteux qui détecte le changement sans payer la récupération — un COUNT(*) rapide ou un horodatage de fichier, pas le SELECT * complet. Si votre checkFunc est aussi coûteux que valueFunc, vous n’avez rien gagné par rapport à un simple minuteur ; rendez la vérification légère pour que la récupération lourde ne se déclenche que lorsque les données ont réellement changé.
Questions fréquentes
Invalidez un réactif selon une planification. À l’intérieur d’un bloc reactive(), observe() ou render*(), appelez invalidateLater(millis, session) — il réexécute ce bloc après millis millisecondes, et comme chaque réexécution le rappelle, le bloc continue de s’actualiser. Pour une horloge, placez invalidateLater(1000, session) en haut d’un renderText() qui lit Sys.time().
invalidateLater(millis) planifie la réexécution du seul bloc dans lequel il est appelé — un tic ad hoc, privé. reactiveTimer(intervalMs) crée un minuteur partagé que vous appelez comme une fonction (autoInvalidate()) dans plusieurs blocs, de sorte qu’ils s’actualisent tous sur le même battement. Utilisez invalidateLater() pour un seul bloc auto-actualisé ; utilisez reactiveTimer() quand plusieurs sorties doivent partager une horloge.
Utilisez reactivePoll() pour une base de données ou une API et reactiveFileReader() pour un fichier. reactivePoll() prend un checkFunc peu coûteux (un COUNT(*) rapide ou un horodatage qui change quand les données changent) et un valueFunc qui récupère les données seulement quand la valeur de checkFunc change — pour que vous n’interrogiez pas à chaque tic. reactiveFileReader() est la même idée précâblée sur la date de modification d’un fichier.
Conditionnez l’actualisation à un drapeau que l’utilisateur contrôle. Gardez un reactiveVal() (par exemple running <- reactiveVal(TRUE)), basculez-le depuis un actionButton() avec observeEvent(), et placez req(running()) avant invalidateLater() dans le bloc. Quand le drapeau vaut FALSE, req() arrête le bloc avant qu’il ne se replanifie, de sorte que le minuteur s’arrête ; rebasculez-le et la prochaine exécution relance le tic.
Ajustez l’intervalle à la vitesse réelle de changement des données — 1 à 5 secondes conviennent à la plupart des tableaux de bord et moniteurs. Chaque tic est un aller-retour serveur et un recalcul par utilisateur, donc un intervalle inférieur à la seconde comme invalidateLater(100) se déclenche dix fois par seconde et par connexion et submerge le server sous charge. N’interrogez pas un flux qui change une fois par minute toutes les secondes.
Testez vos connaissances
Construisez une application avec un textOutput("count") qui s’incrémente une fois par seconde tout seul, plus un actionButton("toggle", "Pause / resume") qui arrête et relance le comptage. En pause, le nombre doit rester stable ; à la reprise, il doit repartir là où il s’était arrêté.
Conservez le compte dans un reactiveVal(0) et un drapeau d’exécution dans un autre reactiveVal(TRUE). Basculez le drapeau avec observeEvent(input$toggle, ...). Dans un observe(), faites d’abord req(running()), puis invalidateLater(1000, session), puis count(isolate(count()) + 1). Enveloppez la lecture dans isolate() pour que l’observateur ne dépende pas de la valeur qu’il écrit.
library(shiny)
ui <- fluidPage(
h2("Pausable counter"),
actionButton("toggle", "Pause / resume"),
textOutput("count")
)
server <- function(input, output, session) {
count <- reactiveVal(0)
running <- reactiveVal(TRUE)
observeEvent(input$toggle, { running(!running()) }) # flip on each click
observe({
req(running()) # paused -> stop before rescheduling
invalidateLater(1000, session) # tick every second
count(isolate(count()) + 1) # increment without depending on itself
})
output$count <- renderText({ paste("Ticks:", count()) })
}
shinyApp(ui, server)req(running()) arrête l’observateur en pause, de sorte que invalidateLater() ne se replanifie jamais et que le compte se fige ; basculer le drapeau permet au prochain tic de reprendre. isolate() lit le compte courant sans rendre l’observateur dépendant de lui, ce qui sinon bouclerait. La sortie lit count() normalement et se met à jour à chaque tic.
Vous voulez qu’une application Shiny affiche des lignes fraîches d’une base de données, mais vérifier la table est un COUNT(*) peu coûteux tandis que récupérer les lignes est coûteux. Quel outil convient le mieux ?
A. invalidateLater() dans un renderTable() qui exécute le SELECT * complet à chaque tic. B. reactiveTimer() partagé entre toutes les sorties, chacune re-interrogeant la base de données. C. reactivePoll() avec un checkFunc peu coûteux (le COUNT(*)) et un valueFunc qui récupère les lignes. D. reactiveFileReader() pointé sur la base de données.
C. reactivePoll() est conçu exactement pour ça : le checkFunc peu coûteux s’exécute à chaque tic pour détecter le changement, et le valueFunc coûteux récupère les lignes seulement quand le compte change — pour que vous n’interrogiez pas à chaque tic. A et B réexécutent la récupération complète à chaque tic (du gaspillage) ; D est la spécialisation fichier, pas pour une base de données.
Conclusion
L’actualisation automatique dans Shiny est une seule idée — invalider un réactif selon une planification — avec quatre outils dimensionnés à la tâche. invalidateLater(millis, session) donne à un seul bloc son propre tic, parfait pour une horloge ou un compteur auto-actualisé. reactiveTimer(intervalMs) est le battement partagé dont plusieurs sorties peuvent dépendre. reactivePoll() surveille une base de données ou une API changeante avec une vérification peu coûteuse et une fonction de valeur récupérée seulement au changement, et reactiveFileReader() fait de même pour un fichier. Quel que soit votre choix, gardez le coût en main : laissez les utilisateurs mettre en pause avec un drapeau reactiveVal() et req(), et choisissez un intervalle qui correspond à la vitesse réelle de changement des données. Avec eux, une application statique devient une application vivante.
Leçons connexes
- Construisez à partir d’ici : tableaux de bord — la leçon précédente ; actualisez les value boxes et les graphiques d’un tableau de bord sur un minuteur ; valeurs réactives — l’état modifiable
reactiveVal()/reactiveValues()qui conserve un compteur ou un flux qui s’accumule entre les tics ; observateurs et événements — les blocsobserve()/observeEvent()qu’un minuteur réexécute. Ensuite, l’interface dynamique construit des éléments d’interface à la volée. - Allez plus loin : Interactivité Shiny — la série complète. · Server Shiny — la couche de réactivité. · Science computationnelle & IA — le pilier.
Réutilisation
Citation
@online{2026,
author = {},
title = {Mises à jour en temps réel Shiny : invalidateLater,
reactiveTimer \& reactivePoll},
date = {2026-06-28},
url = {https://www.datanovia.com/learn/programming/shiny/interactivity/real-time-updates},
langid = {fr}
}