[{"data":1,"prerenderedAt":700},["ShallowReactive",2],{"article-\u002Fblog\u002Fgestion-etat-nuxt-vs-flutter":3},{"id":4,"title":5,"body":6,"brouillon":687,"date":688,"description":689,"extension":690,"image":691,"imageAlt":691,"meta":692,"navigation":573,"path":693,"seo":694,"stem":695,"tags":696,"__hash__":699},"blog\u002Fblog\u002Fgestion-etat-nuxt-vs-flutter.md","Gestion d'état : Nuxt face à Flutter, le parallèle qui éclaire",{"type":7,"value":8,"toc":679},"minimark",[9,13,18,21,38,68,75,79,166,218,235,239,347,351,354,519,522,641,652,656,665,668,672,675],[10,11,12],"p",{},"Je travaille des deux côtés : des applications Flutter au long cours et des\nprojets web sous Nuxt. La bonne nouvelle pour qui connaît déjà l'un, c'est\nque les concepts de gestion d'état de l'autre ne sont pas nouveaux : ce sont\nles mêmes idées sous d'autres noms. Voici la table de correspondance que\nj'aurais aimé avoir en tête plus tôt.",[14,15,17],"h2",{"id":16},"le-socle-réactivité-contre-notification","Le socle : réactivité contre notification",[10,19,20],{},"La différence de fond tient au modèle de réactivité.",[10,22,23,24,28,29,33,34,37],{},"En ",[25,26,27],"strong",{},"Vue\u002FNuxt",", la réactivité est implicite et automatique. Un ",[30,31,32],"code",{},"ref"," est\nune valeur observable, et tout ce qui la lit se réabonne tout seul. Vous\nchangez ",[30,35,36],{},"compteur.value",", l'interface se met à jour, sans rien déclarer.",[10,39,23,40,43,44,47,48,51,52,55,56,59,60,63,64,67],{},[25,41,42],{},"Flutter",", la réactivité est explicite. Un ",[30,45,46],{},"ValueNotifier"," détient une\nvaleur, mais il faut appeler ",[30,49,50],{},"notifyListeners()"," (ou assigner ",[30,53,54],{},".value",") pour\nprévenir, et un widget doit s'abonner explicitement (via ",[30,57,58],{},"ValueListenableBuilder",",\n",[30,61,62],{},"ListenableBuilder",", ou le ",[30,65,66],{},"watch"," de Riverpod).",[10,69,70,71,74],{},"Retenez cette phrase : ",[25,72,73],{},"Vue observe, Flutter notifie",". Tout le reste en\ndécoule.",[14,76,78],{"id":77},"la-brique-de-base","La brique de base",[80,81,86],"pre",{"className":82,"code":83,"language":84,"meta":85,"style":85},"language-typescript shiki shiki-themes github-dark","\u002F\u002F Nuxt \u002F Vue\nconst compteur = ref(0)\nconst doubler = computed(() => compteur.value * 2)\ncompteur.value++   \u002F\u002F l'UI qui lit compteur se met à jour\n","typescript","",[30,87,88,97,125,155],{"__ignoreMap":85},[89,90,93],"span",{"class":91,"line":92},"line",1,[89,94,96],{"class":95},"sAwPA","\u002F\u002F Nuxt \u002F Vue\n",[89,98,100,104,108,111,115,119,122],{"class":91,"line":99},2,[89,101,103],{"class":102},"snl16","const",[89,105,107],{"class":106},"sDLfK"," compteur",[89,109,110],{"class":102}," =",[89,112,114],{"class":113},"svObZ"," ref",[89,116,118],{"class":117},"s95oV","(",[89,120,121],{"class":106},"0",[89,123,124],{"class":117},")\n",[89,126,128,130,133,135,138,141,144,147,150,153],{"class":91,"line":127},3,[89,129,103],{"class":102},[89,131,132],{"class":106}," doubler",[89,134,110],{"class":102},[89,136,137],{"class":113}," computed",[89,139,140],{"class":117},"(() ",[89,142,143],{"class":102},"=>",[89,145,146],{"class":117}," compteur.value ",[89,148,149],{"class":102},"*",[89,151,152],{"class":106}," 2",[89,154,124],{"class":117},[89,156,158,160,163],{"class":91,"line":157},4,[89,159,36],{"class":117},[89,161,162],{"class":102},"++",[89,164,165],{"class":95},"   \u002F\u002F l'UI qui lit compteur se met à jour\n",[80,167,171],{"className":168,"code":169,"language":170,"meta":85,"style":85},"language-dart shiki shiki-themes github-dark","\u002F\u002F Flutter\nfinal compteur = ValueNotifier\u003Cint>(0);\ncompteur.value++;  \u002F\u002F les ValueListenableBuilder abonnés se reconstruisent\n","dart",[30,172,173,178,206],{"__ignoreMap":85},[89,174,175],{"class":91,"line":92},[89,176,177],{"class":95},"\u002F\u002F Flutter\n",[89,179,180,183,186,189,192,195,198,201,203],{"class":91,"line":99},[89,181,182],{"class":102},"final",[89,184,185],{"class":117}," compteur ",[89,187,188],{"class":102},"=",[89,190,191],{"class":106}," ValueNotifier",[89,193,194],{"class":117},"\u003C",[89,196,197],{"class":106},"int",[89,199,200],{"class":117},">(",[89,202,121],{"class":106},[89,204,205],{"class":117},");\n",[89,207,208,210,212,215],{"class":91,"line":127},[89,209,36],{"class":117},[89,211,162],{"class":102},[89,213,214],{"class":117},";  ",[89,216,217],{"class":95},"\u002F\u002F les ValueListenableBuilder abonnés se reconstruisent\n",[10,219,220,221,223,224,226,227,230,231,234],{},"Le ",[30,222,32],{}," de Vue correspond au ",[30,225,46],{}," de Flutter : une valeur\nobservable. Le ",[30,228,229],{},"computed"," de Vue, une valeur dérivée qui se recalcule seule,\ncorrespond à un ",[30,232,233],{},"Provider"," dérivé de Riverpod ou à un getter recalculé. Dans\nles deux mondes, on ne recopie jamais une valeur dérivée à la main : on la\ndéclare comme dépendant d'une autre.",[14,236,238],{"id":237},"la-table-de-correspondance","La table de correspondance",[240,241,242,263,278,298,316,330],"ul",{},[243,244,245,253,254,262],"li",{},[25,246,247,249,250],{},[30,248,32],{}," \u002F ",[30,251,252],{},"reactive"," (Vue) correspond à ",[25,255,256,258,259],{},[30,257,46],{}," \u002F\n",[30,260,261],{},"ChangeNotifier"," (Flutter) : la valeur observable de base.",[243,264,265,269,270,273,274,277],{},[25,266,267],{},[30,268,229],{}," correspond à un ",[25,271,272],{},"provider dérivé"," (Riverpod) ou un\n",[25,275,276],{},"getter"," recalculé : l'état dérivé.",[243,279,280,287,288,293,294,297],{},[25,281,282,249,284],{},[30,283,66],{},[30,285,286],{},"watchEffect"," correspond à ",[25,289,290],{},[30,291,292],{},"ref.listen"," (Riverpod) ou à\nl'écoute d'un ",[30,295,296],{},"Listenable"," : réagir à un changement pour déclencher un\neffet.",[243,299,300,305,306,311,312,315],{},[25,301,302],{},[30,303,304],{},"useState"," (état partagé Nuxt) correspond à un ",[25,307,308,310],{},[30,309,233],{}," global","\n(Riverpod) ou à un ",[30,313,314],{},"ChangeNotifierProvider"," (Provider) : un état accessible\ndepuis plusieurs endroits.",[243,317,318,321,322,325,326,329],{},[25,319,320],{},"Pinia"," (store avec state, actions, getters) correspond à ",[25,323,324],{},"Riverpod\nstructuré"," ou à un ensemble de ",[25,327,328],{},"Cubit\u002FBLoC"," : le domaine métier avec sa\nlogique.",[243,331,332,223,341,346],{},[25,333,334,335,249,338],{},"L'injection par ",[30,336,337],{},"provide",[30,339,340],{},"inject",[25,342,343,345],{},[30,344,233],{},"\nd'arbre de widgets"," de Flutter : fournir une dépendance à un sous-arbre.",[14,348,350],{"id":349},"un-store-deux-écritures","Un store, deux écritures",[10,352,353],{},"Un panier, côté Pinia :",[80,355,357],{"className":82,"code":356,"language":84,"meta":85,"style":85},"\u002F\u002F stores\u002Fpanier.ts\nexport const usePanier = defineStore('panier', () => {\n  const articles = ref\u003CArticle[]>([])\n  const total = computed(() =>\n    articles.value.reduce((s, a) => s + a.prix, 0)\n  )\n  function ajouter(a: Article) { articles.value.push(a) }\n  return { articles, total, ajouter }\n})\n",[30,358,359,364,394,414,430,470,476,504,513],{"__ignoreMap":85},[89,360,361],{"class":91,"line":92},[89,362,363],{"class":95},"\u002F\u002F stores\u002Fpanier.ts\n",[89,365,366,369,372,375,377,380,382,386,389,391],{"class":91,"line":99},[89,367,368],{"class":102},"export",[89,370,371],{"class":102}," const",[89,373,374],{"class":106}," usePanier",[89,376,110],{"class":102},[89,378,379],{"class":113}," defineStore",[89,381,118],{"class":117},[89,383,385],{"class":384},"sU2Wk","'panier'",[89,387,388],{"class":117},", () ",[89,390,143],{"class":102},[89,392,393],{"class":117}," {\n",[89,395,396,399,402,404,406,408,411],{"class":91,"line":127},[89,397,398],{"class":102},"  const",[89,400,401],{"class":106}," articles",[89,403,110],{"class":102},[89,405,114],{"class":113},[89,407,194],{"class":117},[89,409,410],{"class":113},"Article",[89,412,413],{"class":117},"[]>([])\n",[89,415,416,418,421,423,425,427],{"class":91,"line":157},[89,417,398],{"class":102},[89,419,420],{"class":106}," total",[89,422,110],{"class":102},[89,424,137],{"class":113},[89,426,140],{"class":117},[89,428,429],{"class":102},"=>\n",[89,431,433,436,439,442,446,449,452,455,457,460,463,466,468],{"class":91,"line":432},5,[89,434,435],{"class":117},"    articles.value.",[89,437,438],{"class":113},"reduce",[89,440,441],{"class":117},"((",[89,443,445],{"class":444},"s9osk","s",[89,447,448],{"class":117},", ",[89,450,451],{"class":444},"a",[89,453,454],{"class":117},") ",[89,456,143],{"class":102},[89,458,459],{"class":117}," s ",[89,461,462],{"class":102},"+",[89,464,465],{"class":117}," a.prix, ",[89,467,121],{"class":106},[89,469,124],{"class":117},[89,471,473],{"class":91,"line":472},6,[89,474,475],{"class":117},"  )\n",[89,477,479,482,485,487,489,492,495,498,501],{"class":91,"line":478},7,[89,480,481],{"class":102},"  function",[89,483,484],{"class":113}," ajouter",[89,486,118],{"class":117},[89,488,451],{"class":444},[89,490,491],{"class":102},":",[89,493,494],{"class":113}," Article",[89,496,497],{"class":117},") { articles.value.",[89,499,500],{"class":113},"push",[89,502,503],{"class":117},"(a) }\n",[89,505,507,510],{"class":91,"line":506},8,[89,508,509],{"class":102},"  return",[89,511,512],{"class":117}," { articles, total, ajouter }\n",[89,514,516],{"class":91,"line":515},9,[89,517,518],{"class":117},"})\n",[10,520,521],{},"Le même panier, côté Cubit (flutter_bloc) :",[80,523,525],{"className":168,"code":524,"language":170,"meta":85,"style":85},"class PanierCubit extends Cubit\u003CList\u003CArticle>> {\n  PanierCubit() : super([]);\n\n  int get total => state.fold(0, (s, a) => s + a.prix);\n\n  void ajouter(Article a) => emit([...state, a]);\n}\n",[30,526,527,553,569,575,610,614,636],{"__ignoreMap":85},[89,528,529,532,535,538,541,543,546,548,550],{"class":91,"line":92},[89,530,531],{"class":102},"class",[89,533,534],{"class":106}," PanierCubit",[89,536,537],{"class":102}," extends",[89,539,540],{"class":106}," Cubit",[89,542,194],{"class":117},[89,544,545],{"class":106},"List",[89,547,194],{"class":117},[89,549,410],{"class":106},[89,551,552],{"class":117},">> {\n",[89,554,555,558,561,563,566],{"class":91,"line":99},[89,556,557],{"class":106},"  PanierCubit",[89,559,560],{"class":117},"() ",[89,562,491],{"class":102},[89,564,565],{"class":106}," super",[89,567,568],{"class":117},"([]);\n",[89,570,571],{"class":91,"line":127},[89,572,574],{"emptyLinePlaceholder":573},true,"\n",[89,576,577,580,583,586,588,591,594,596,598,601,603,605,607],{"class":91,"line":157},[89,578,579],{"class":106},"  int",[89,581,582],{"class":102}," get",[89,584,585],{"class":117}," total ",[89,587,143],{"class":102},[89,589,590],{"class":117}," state.",[89,592,593],{"class":113},"fold",[89,595,118],{"class":117},[89,597,121],{"class":106},[89,599,600],{"class":117},", (s, a) ",[89,602,143],{"class":102},[89,604,459],{"class":117},[89,606,462],{"class":102},[89,608,609],{"class":117}," a.prix);\n",[89,611,612],{"class":91,"line":432},[89,613,574],{"emptyLinePlaceholder":573},[89,615,616,619,621,623,625,628,630,633],{"class":91,"line":472},[89,617,618],{"class":102},"  void",[89,620,484],{"class":113},[89,622,118],{"class":117},[89,624,410],{"class":106},[89,626,627],{"class":117}," a) ",[89,629,143],{"class":102},[89,631,632],{"class":113}," emit",[89,634,635],{"class":117},"([...state, a]);\n",[89,637,638],{"class":91,"line":478},[89,639,640],{"class":117},"}\n",[10,642,643,644,647,648,651],{},"La forme diffère (Pinia expose un objet réactif, le Cubit émet un nouvel\nétat immuable), mais la structure est identique : un état, une valeur\ndérivée (",[30,645,646],{},"total","), une action (",[30,649,650],{},"ajouter","). Un développeur Flutter lit le\nstore Pinia sans effort, et réciproquement.",[14,653,655],{"id":654},"la-dimension-que-flutter-na-pas-le-serveur","La dimension que Flutter n'a pas : le serveur",[10,657,658,659,661,662,664],{},"Il y a un endroit où le parallèle s'arrête, et c'est important. Nuxt fait du\nrendu serveur : le même code de gestion d'état s'exécute côté serveur puis\ncôté client. D'où la nécessité de ",[30,660,304],{}," plutôt qu'un simple ",[30,663,32],{},"\nglobal, pour isoler l'état par requête et éviter qu'un visiteur voie les\ndonnées d'un autre (j'en parle dans l'article sur les bonnes pratiques).",[10,666,667],{},"Flutter n'a pas ce problème : une application tourne sur un seul appareil,\npour un seul utilisateur. Un singleton d'état y est parfaitement légitime.\nEn Nuxt, le même singleton est un bug de sécurité. C'est la principale\nadaptation mentale à faire en passant de Flutter au web rendu côté serveur.",[14,669,671],{"id":670},"la-leçon","La leçon",[10,673,674],{},"Ne réapprenez pas la gestion d'état, traduisez-la. « Observer une valeur »,\n« en dériver une autre », « réagir à un changement », « partager un\ndomaine » : ces quatre gestes existent des deux côtés, avec des noms\ndifférents. Gardez en tête que Vue observe quand Flutter notifie, et\nn'oubliez jamais la dimension serveur de Nuxt, absente de Flutter. Avec ces\ndeux repères, passer d'un monde à l'autre devient une simple traduction, pas\nun nouvel apprentissage.",[676,677,678],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":85,"searchDepth":99,"depth":99,"links":680},[681,682,683,684,685,686],{"id":16,"depth":99,"text":17},{"id":77,"depth":99,"text":78},{"id":237,"depth":99,"text":238},{"id":349,"depth":99,"text":350},{"id":654,"depth":99,"text":655},{"id":670,"depth":99,"text":671},false,"2026-07-16","Pour un développeur Flutter qui apprend Nuxt (ou l'inverse) : la correspondance entre ref, computed, useState, Pinia et ValueNotifier, Provider, Riverpod, Bloc.","md",null,{},"\u002Fblog\u002Fgestion-etat-nuxt-vs-flutter",{"title":5,"description":689},"blog\u002Fgestion-etat-nuxt-vs-flutter",[697,42,698],"Nuxt","Gestion d'état","scuvq3Ifi-uadXy9ziFqGIkbxMqEKu9B-vLJ-MLkUsU",1784652014639]