Une brève histoire du frontend
Au cours des 14 dernières années, ou plutôt 15 dans quelques jours, le développement front-end a vécu quelques bouleversements. Pendant ce temps, je n’étais pas là.
Durant ce temps, j’étais loin du développement. Je commençais mes études en économie et j’appliquais mes formules du Lagrangien. J’apprenais à optimiser sous contrainte des équations à plusieurs inconnues pour comprendre les agents économiques. L’économie mathématique, c’était sympa, mais j’ai vite découvert une discipline qui m’a bien plus passionné : l’histoire des faits et de la pensée. Au fond, j’avais la curiosité de comprendre la genèse d’une théorie économique, à quelles conditions elle émerge et comment elle évolue, modifie nos perceptions de la réalité et notre façon de penser les problèmes. Cette information va compter pour la suite de cet article.
Alors j’ai suivi cette voie, un temps. À côté de cela, je programmais déjà depuis longtemps, mais j’étais vraiment mauvais. Petit retour en arrière. Mais avant, retour en 2006. Comme beaucoup de monde à cette époque, j’ai appris très jeune avec le Site Du Zéro et puis je me suis lancé dans des projets, sans jamais vouloir faire carrière dans l’informatique. C’était une passion, celle de construire des choses pour presque rien : le PC familial et c’est tout.
Pendant mes études d’économie et ma courte carrière d’enseignant, je n’ai pas suivi les actualités en matière de développement. Je suis parti quand les bouleversements sont arrivés, courant 2010. À ce moment, je faisais beaucoup plus d’applications lourdes en C#/.net pour moi-même.
Je ne suis réellement revenu qu’au web depuis 3 ans, courant 2021, avec de plus ou moins gros projets. C’est là que je me suis rendu compte du changement opéré pendant cette brève décennie. Plus rien n’était pareil dans le monde du front-end. Comme un développeur peu malin et plus tourné vers le back-end, je sous-estimais fortement le front-end : ce n’est que trois balises HTML et un peu de JS". Je sais, la position est très bête, c’est dire mon ignorance totale des défis rencontrés par le front-end dit “moderne”.
Quelques mois plus tard, ou quelques années, ressurgit une question : comment en est-on arrivé là ? Comment le front-end est passé d’un simple document HTML relativement simple à React qui domine le marché ? Je vous l’avais dit, cette histoire d’études n’était pas pour rien.
Ces derniers mois, j’ai développé une passion grandissante pour le front-end, non pas parce que j’aime la conception d’interfaces, mais parce que c’est un nouveau modèle de pensée, bien plus complexe que je ne l’imaginais. En tant que développeur, j’ai beaucoup progressé grâce à ces découvertes front-end.
Je vous propose donc aujourd’hui une brève histoire du front-end, ou comment le front-end est devenu un domaine très technique dans le développement web. Tout cela s’appuie sur des interviews, des historiques, des articles de blog, etc. Je ne pourrais malheureusement pas sourcer tous mes propos, mais je vais essayer d’être le plus exhaustif possible.
Au fond, le cœur de cet article n’est pas tant l’histoire du front-end moderne qu’un exemple concret d’un concept plus large que j’identifierais naïvement par “le code comme littérature et construction sociale structurée et structurante”.
Pourquoi 2010 ?
J’ai très rapidement évoqué l’année 2010 comme point de bascule sur le front-end, mais qu’est-ce qu’il se passe précisement en 2010 qui permet de provoquer ce boulversement ?
Il faut prendre cette année plutot comme une estimation pratique, plutot qu’une date fixe. La plupart des projets dont on parlera naissent après cette date, de quelques années.
D’un point de vue historique c’est une date qui apporte de beaux changements.
Le premier notable c’est l’apparition du moteur JS de Google, V8 fin 2008, qui offre aux navigateurs sur le marché un gain de performance notable. C’est la première vague d’optimisation notable que JS va vivre et c’est un prérequis aux changement de paradigme front-end qui va arriver. Je n’ai pas trouvé de benchmark satisfaisant pour montrer ce gain de performance, reste qu’il est notable et à assis la domination de Chrome/Chromium. On a donc un gain de puissance relative par rapport à avant. Avec le même matériel on est en capacité d’effectuer plus d’opérations JS.
En même temps les machines gagnent en puissance brute avec les processeurs intel de la serie i (i3, i5, i7). L’ordinateur de tout un chacun est en capacité de faire plus.
Et dernier grand point, c’est le moment où les GAFAM atteignent des masses critiques en terme d’infrastructure qui impose de grand changements. Facebook est en croissance constante d’inscriptions et d’utilisateurs actifs. Le web devient de plus en plus utilisé et les cas d’usage demandent de plus en plus d’interactivité.
La complexité grandissante du front-end
Les interfaces s’étoffent et demandent de la réactivité : le petit bouton des likes qui s’anime et son chiffre qui augmente en temps réel sans rechargement de la page, les commentaires sous une publication qui apparaissent en live ou bien encore un simple flux au scroll infini. Ce standard qui n’en est pas encore un s’impose de plus en plus.
A cette époque et depuis 2006, jQuery
est la librairie qui sauve la vie des développeurs. Elle rend plus facile la selection et la modification d’élements dans le DOM avec une relativement faible surcharge pour le client. Animer des pages web devient beaucoup plus facile et tout le monde s’y met.
Si vous travaillez avec du legacy, il y a une chance sur deux pour que votre front-end utilise encore jQuery
.
Simplifier ne veut pas pour autant dire rendre la vie plus facile. Et on se retrouve avec des pages qui deviennent de plus en plus difficile à maintenir puisque la logique de modification est déporté à plusieurs localisations. Il est difficile de penser en composant isolé, et bien souvent pour effectuer plusieurs modifications en même temps sur le même élément, la rigueur est nécessaire.
Quand plusieurs élements HTML dépendent l’un de l’autre, admettons un formulaire avec un select
qui modifie le contenu d’un autre select
puis une vue plus bas, typiquement un choix de pays, puis de ville, etc, la capacité à ajouter d’autres intéractions dans l’interface sans créer de bugs devient presque nulle.
Deux problèmes sous-jacents peuvent expliquer cette complexité :
- On perd l’état de l’application en modifiant directement les élements, et/ou la gestion des états est vite chaotique. Une fois que j’ai modifié ma div #comment-123, comment est-ce que je peux connaitre son contenu ? Je peux seulement le savoir en regardant son contenu, mais pour cela, il faut avoir retenu l’avoir crée. L’autre solution pourrait être de conserver une liste des commentaires, en plus de l’ajout de cette div au DOM, qui stocke chaque commentaire. Cela implique une double saisie qui peut vite casser.
- Les structures de données ne sont pas immutables et donc la modification d’une donnée peut avoir des conséquences sur une autre sans que l’on puisse s’en rendre compte. Que se passe-t-il si une fonction, pour quelque raison que ce soit modifie par inadvertance la variable qui stocke l’état de mon application ? Je ne peux pas le savoir, et cela peut arriver.
Entendons-nous, pour des applications de petite taille, cela n’est pas très problématique. L’expérience en tant que développeur n’est pas plaisante mais elle n’est pas non plus déplorable. Cela se fait. Mais imaginos devoir gérer les changements d’état du flux d’actualité de facebook ou de n’importe quel réseau social, ou bien d’une application de gestion du personnel avec la liste, l’ajout, la modification, etc, comme une application native sans la contrainte du rechargement de la page ? Cela devient compliqué à maintenir et un enfer à développer.
Je sais que vous sentez venir l’histoire de React
, et bien non, ce n’est pas pour tout de suite.
React répond à ses problèmes mais il n’est pas le premier ni le plus adapté.
L’emergance de la transpilation des langages fonctions vers JavaScript
Pour répondre à une partie des problématiques que rencontre les développeurs JS, des initiatives de langages qui transpilent vers JS apparaissent. L’idée est de créer des langages ou bien de fournir une conversion de certains langages vers JavaScript, faisant profiter les développeurs de la sécurité de ceux-ci tout en permettant une sortie compatible avec le navigateur. JS est perçu comme une plateforme vers laquelle “compiler”.
La plus célèbre de ces critiques née le 27 Janvier 2012 dans “The JavaScript Problem” 1 sur le wiki Haskell.
Je vous laisse lire ce merveilleux premier jet:
The problem
The JavaScript problem is two-fold and can be described thus:
‘‘‘JavaScript sucks.’’’ The depths to which JavaScript sucks is well-documented and well-understood. Its main faults are: its lack of module system, verbose function syntax¹, late binding², which has led to the creation of various static analysis tools to alleviate this language flaw³, but with limited success⁴ (there is even a static type checker⁵), finicky equality/automatic conversion, this behaviour, and lack of static types.
‘‘‘We need JavaScript.’’’ Using it for what it is good for, i.e. providing a platform for browser development, but not using the language ‘‘per se’’, is therefore desirable, and many are working to achieve this, in varying forms. There various ways to do it, but we ought to opt for compiling an existing language, Haskell, to JavaScript, because we do not have time to learn or teach other people a new language, garner a new library set and a new type checker and all that Haskell implementations provide. Source : wiki.haskell.org/index.php?title=The_JavaScript_Problem&diff=66741&oldid=44243
La critique est sévère mais juste. Le manque de typage statique et de fonctionnalités avancées se fait sentir mais il n’est pas question de se passer purement et simple du langage. Naturellement, pour répondre aux critiques la solution proposée est…Haskell. Sur le wiki Haskell, ce n’est guère étonnant.
Ce constat est partagé par la communauté qui est proche des langages fonctionnels, quoi de plus étonnant que tout ce qui fait la force de ceux-ci leur manque dans JS ?
Plusieurs alternatives vont ainsi voir le jour, où on déjà vu le jour depuis quelques mois. J’aimerais en noter quelques uns, sans vocation à être exhaustif:
Js_of_Ocaml
: Son premier commit date du 1 février 2010 et a son importance pour la suite.- CoffeeScript, le fameux. Inspiré de l’expressivité de Ruby mais aussi d’élements de Haskell, c’est personnellment un des rares que je connaissais de réputation, n’était pas dans les milleux de la programmation fonctionnelle. Il débarque fin 2009, début 2010.
- PureScript qui voit le jour en 2013, forcement inspiré d’Haskell.
- Elm, qui est le fruit d’une thèse et sort en 2012, encore une fois très proche d’Haskell (Elm est écrit en Haskell !).
- Et tous les autres que je ne peux pas citer.
C’est ainsi que commence une grande tradition des transpilers vers JS, qui existe encore aujourd’hui, peut-être avec le plus connu : TypeScript. Il sort relativement tardivement dans sa version 1, en 2014 mais existe déjà dépuis 2010 chez Microsoft, tout en étant publiquement accessible depuis 2012. Son succès est tel qu’aujourd’hui TypeScript est synomyme de JavaScript pour beaucoup, tellement son usage est nécessaire et non négociable. Pourtant, TypeScript n’est pas une langage fonctionnel, il ne fait qu’en emprunter des élements.
Quels qu’ils soient, tous règlent le problème de l’immutabilité et du typage statique (déclaré ou inféré) mais ne répondent en rien au problème de la gestion de l’état de l’application.
L’emergence d’un nouveau modèle de gestion des états.
Mais en fait c’est quoi en état ? Un état est une valeur, à un moment donnée, pour un élement d’un composant d’une application. Par exemple, dans le cas d’un bouton qui affiche le nombre de fois où il a été cliqué, alors l’état est le nombre de clics.
Plus globalement et par abus de langage, l’état d’une application représente l’ensemble des données qui peuvent être modifiées et qui ont un effet sur l’affichage (directement ou non) de celle-ci.
A mon sens, deux modèles de gestion des états se distinguent :
- L’approche de Elm : Model, Update, View
- L’approche de React : Components et State.
Les deux partagent un même constat qui est fondamental des évolutions dont on parlera : il vaut mieux faire un nouveau rendu total de la page que de chercher à mettre à jour un seul élément.
Cette méthode de la table rase
est contre-intuitive et n’emporte pas beaucoup d’adhésion à ses débuts, loin de là.
J’omets volontairement les frameworks ou librairies d’inspiration MVC qui se développent à cette époque, en ne citant que Backbone.js
dans cette veine. Ce n’est pas le point de ce court article.
Créer un langage ou un framework, c’est avant tout choisir quelles sont les bonnes pratiques que l’on décidera de rendre facile à écrire et quelles mauvaises pratiques l’on choisir de rendre difficile à écrire. Developer Voices - https://www.youtube.com/watch?v=0SUM4869ODc
La Elm Architecture
Parole de fanboy, je considère que l’apport de Elm au monde du front-end est gigantesque, et plus généralement que l’approche d’Evan Czaplicki frôle la perfection. C’est un juste équilibre entre simplicité conceptuelle et efficacité technique. Des nombreuses interviews et conférences que l’on peut retrouver de lui, la maxime d’Elm pourrait se résumer à :
Faire une seule chose, la faire bien et la faire la plus simplement possible.
Elm est un langage et à la fois framework, conçu pour créer des interfaces web dynamiques en toute sécurité. Au grnad désespoir de ses fans, Elm n’existe pas pour le back-end, et ne peux que concevoir des interface web.
Le schéma d’execution d’une application Elm est très simple :
- Un évenement est déclanché (init, UserClickedSignIn, UserClickedMorePosts, etc).
- La fonction
update
est appélée avec cet évenement (qu’on appellemessage
), et vient modifier l’état de l’application (Model
), suivant nos règles définies. - La fonction
view
est appéelée, avec l’état de l’application actuelle en paramètre. Et ne s’occupe que de retourner la vue.
Au démarrage, la fonction init
est appelée, avec une initialisation de l’état global et un evenement (vide ou non). Ensuite l’application est affichée.
A chaque évenement, un nouvel état est crée, remplaçant l’ancien, et la vue régénérée. Ensuite, un peu de magie et de DOM virtuel vient modifier le contenu HTML de la page, et évite de réellement tout réécrire.
Rien que ce modèle est un vrai changement de paradigme pour le développeur web moyen, qui connait pas ou peu le monde de la programmation fonctionnelle, mais Elm va bien plus loin. Etant un langage à part entière, il définit ses propres règles et apporte un typage statique fort avec inférence, répondant à un des points du “The JavaScript Problem”.
L’inférence des types seule n’est pas en soit une grande aide si elle ne vient pas avec des messages d’erreur explicites et utiles. C’est une autre grande force d’Elm. Chaque message d’erreur est pensé pour être compréhensible et permettre la résolution de celle-ci. On est loin d’une stack trace
de n’importe quel langage, voire pire, des erreurs de JS.
Un exemple est plus parlant que tout propos évangélique :
-- TYPE MISMATCH -------------------------------------------------- src/Main.elm
The 1st argument to `value` is not what I expect:
41| [ input [ placeholder "Rechercher...", value model.content, onInput Change ] []
^^^^^^^^^^^^^
The value at .content is a:
Int
But `value` needs the 1st argument to be:
String
Hint: Want to convert an Int into a String? Use the String.fromInt function!
Pour cet exemple j’ai simplement modifié le type d’une propriété de mon Model
, de String
en Int
. Elm refuse de compiler et affiche cette erreur.
Que dire ? La solution est présente dans le retour du compilateur. Je ne peux pas afficher un Int
sans le convertir avant, les élements HTML n’acceptant que des String
.
*(Aucune conversion implicite n’est permise, autre point du JS Problem ;) )
Aujourd’hui cela peut vous semblez normal si vous utilisez dans langages avec un bon tooling mais ce n’était pas la norme à cette époque.
Un autre grand avantage d’Elm peut se comprendre avec cet exemple : tout code qui compile est un code qui n’aura pas ou peu de bug en production. C’est un autre grand changement par rapport au JS traditionnel qui est plus capricieux.
Bon, mais ça ressemble à quoi Elm ? A ça :
module Main exposing (..)
import Browser
import Html exposing (Html, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
-- MAIN
main: Program() Model Msg
main =
Browser.sandbox { init = init, update = update, view = view}
-- MODEl
type alias Model =
{ content : String
}
init : Model
init =
{ content = ""
}
-- UPDATE
type Msg
= Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change search ->
{ model | content = search}
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Rechercher...", value model.content, onInput Change ] []
]
Sans plus de commentaire, on comprend aisement le changement de paradigme que cela impose aux développeurs qui ont une expérience en langage dérivé/hérité du C.
- Absence de parenthèse ou d’accolade.
- Indentation importante.
- La déclaration des types d’une fonction se fait au dessus de sa déclaration comme en
Haskell
. - Types algébriques (qu’on ne voit pas vraiment ici).
- Chaque élement HTML est une fonction à deux paramètres : attributs et enfants. La syntaxe est plus complexe au premier abord que de l’HTML classique, mais permet une sécurité des types.
Vous imaginez bien que le développeur JS moyen n’est pas très enchanté de passé sur Elm, la courbe d’apprentissage va être difficile.
D’autant plus que rappelez-vous, les variables sont immutables ! On oublie les boucles qui modifies des élements sur le même tableau, etc.
Quid aussi de la capacité à utilisrèser des librairies externes, par exemple pour génère des SVG, graphiques, etc ?
Elm intègre ce qu’on appelle des ports
, qui permet de faire appel à des librairies JS, comme une FFI
dans les autres langages, mais cela n’est pas aussi pratique que d’appeler directement comme en JavaScript natif, la librairie en question.
Malgré les qualités incontestable d’Elm, ses barrières à l’entrée sont très fortes et son succès actuel, inexistant, est la meilleure façon de les mesurer.
Mes les idées fonctionnelles n’ont pas perdu pour autant, le champion incontestable arrive.
React
Qui a besoin de présenter React aujourd’hui ? C’est la librairie front-end de la décenie. Je m’éviterais d’en faire une présentation pour me concentrer sur ce qu’il apporte de différent et novateur.
On doit le projet initial à une personne plutôt discrète, Jordan Walke. Peu d’interviews, peu de conférences et peu de tout. Je ne suis même pas sûr qu’une grande partie des développeurs React connaissent son nom.
Tout commence chez Facebook, dans l’équipe Ads. Ils rencontrent les mêmes problèmes que partout ailleurs : les interfaces deviennent de plus en plus complexes à concevoir. Jordan Walke travaille sur une idée : pourquoi ne pas re-afficher totalement la page ou bien le composant à chaque changement ?
Deuxième personnage important qui estime que c’est une bonne idée. Le schéma peut devenir récurrent.
Si vous avez un peu étudié l’économique, vous devez reconnaître un motif ici, quelque chose…Je vous donne un indice : Walras, Menger et Jevons. Vous l’avez ? Pour les autres, un mouvement nommé le “marginalisme” émerge en Europe dans plusieurs pays différents sur une période 30 ans qui va structurer le courant économique le plus influent de l’histoire, celui de l’école néoclassique. Avec peu de liens et aucune connaissance notable des travaux des uns et des autres, ces trois auteurs vont théoriser des lois finalement très proches, dans le même objectif, proposer un théorique de la valeur pour expliquer des phénomènes jusqu’à présents difficile comprendre.
Avec Czaplicki et Walke, je retrouve ce goût du marginalisme. Tout deux proposent une solution similaire à un problème rencontré.
Jordan Walke est un développeur différent des autres, avec une tendance à la programmation fonctionnelle et c’est ce qu’il affirme dans une interview très utile pour cet article2:
[..]For the longest time I just assumed “welp, I guess I’m just a weird programmer”. Then I finally took a course on programming language fundamentals (which used ML (SML) for much of the coursework) and I finally had some basic terminology to be able describe how I wanted to build applications. I also learned that the style of programming that I gravitated towards was neither weird, nor new and actually closer to some of the oldest philosophies of programming languages - philosophies which hadn’t ever become mainstream - philosophies that the industry had spent upwards of twenty years undermining (in my opinion, to their disadvantage). Jordan Walke 2
Ici, il fait directement référence à deux éléments :
- Tout est une fonction pure qui retourne un résultat identitique pour un état identique.
- Tout est immutable.
Cette conception induit donc que tout élement, par exemple un bouton, est une fonction qui retourne un élement HTML (ou JSX dans notre cas).
N.B: On parle ici de fonction pure quand elle ne produit pas d’effet externe sur l’application, autrement dit qu’elle ne modifie rien d’autre que ses variables internes.
Un exemple de fonction pure, en reprenant notre bouton serait :
function button(label) {
return `<button>${label}</button>`;
}
Dans le cas des interfaces graphiques et de l’interactivité, si mon utilisateur clique sur un bouton c’est pour executer quelque chose : soumettre un formulaire, appliquer un changement, etc.
Dès lors, je vais devoir mettre à jour ma page à l’aide d’un querySelector
ou tout autre méthode. Ma fonction qui s’en charge n’est plus pure, elle produit un effet externe.
Penser une interface en fonctions pures, implique deux choses :
- Penser l’interface globale comme fonction pure
- Découper l’interface en composants qui sont des fonctions pures elles-mêmes.
Si vous avez suivi jusqu’ici, vous vous dites surement que c’est strictement l’approche d’Elm. Oui, c’est bien ça, mais ici on reste dans le JavaScript, sans passer par un autre langage.
Vous ne trouvez pas que tout ça, ça fait beaucoup de fonctions à déclarer ? Et déclarer des fonctions en JavaScript c’est verbeux.
function myApp(title, content) {
return titleSection(title) + contentSection(content);
}
function titleSection(title) {
return `<h1>${title}</h1>`;
}
function contentSection(content) {
return `<p>${content}</p>`;
}
const render = myApp('Hello', 'Here the content');
document.getElementById('#app').innerHTML = render;
Depuis ES6 en 2015, il est possible de déclarer des fonctions avec une syntaxe raccourcis, qu’on appelle les arrow fucntions
.
const titleSection = (title) => `<h1>${title}</h1>`;
const contentSection = (content) => `<p>${content}</p>`;
const myApp = (title, content) => titleSection(title) + contentSection(content);
const render = myApp('Hello', 'Here the content');
document.getElementById('#app').innerHTML = render;
Moins de code boilerplate
et tout aussi agréable à lire.
Pour l’exercice, voici un exemple avec le classique counter
, qui suit cette logique: (Essayer ici)
const Button = (count) => `
<button msg="increment">Cliqué ${count} fois</button>
`;
// Un seul composant dans l'app pour cet exemple
const App = (state) => Button(state.count);
// Elm quand tu nous tiens... Des états immutables ! On crée un nouvel objet d'état
const update = (state, action) => {
switch (action) {
case "increment":
return { ...state, count: state.count + 1 };
default:
return state;
}
};
// State intial, comme objet.
let state = { count: 0 };
// Rappel, on veut des composants purs, donc une autre fonction doit être impure (= avoir des effets extérieurs)
const render = (state) => {
document.getElementById('app').innerHTML = App(state);
};
// Event Listener pour écouter les évenements et propager le changement d'état
document.addEventListener("click", (e) => {
if (e.target.getAttribute("msg") === "increment") {
state = update(state, "increment");
render(state);
}
});
// Initial Render
render(state);
Vous me direz que React ne fonctionne pas avec une fonction d’update
mais par des hooks
.
Effectivement, ce sont ces hooks
qui vont permettre d’écouter la modification des valeurs de l’état, et d’effectuer un nouveau rendu si nécessaire.
Mais en prenant un peu de hauteur, on pourrait schématiser les hooks
comme des fonctions d’update
définies localement, qui appelent un render local.
Si on reprend le code du dessus, voilà ce qu’il pourrait donner : (Essayer ici)
// Init hook system
let currentComponent = null;
const hooks = [];
let hookIndex = 0;
// useState *like* React but simpler
function useState(initialValue) {
const index = hookIndex;
if (!hooks[index]) {
hooks[index] = initialValue; // Initialize state if not set
}
const setState = (newValue) => {
hooks[index] = typeof newValue === "function" ? newValue(hooks[index]) : newValue;
render(); // Re-render the full UI
};
hookIndex++;
return [hooks[index], setState];
}
// Button component using a hook
const Button = () => {
const [count, setCount] = useState(0);
// Little trick to put increment to global scope. React handle this way better
window.increment = () => {
setCount((prev) => prev + 1);
};
return `
<button onclick="increment()">Cliqué ${count} fois</button>
`;
};
// Root application component
const App = () => {
// Unsed in this example but usefull for multiple components pages
currentComponent = App;
hookIndex = 0;
return Button();
};
// Render function
const render = () => {
document.getElementById('app').innerHTML = App();
};
// Initial render
render();
En peu d’étapes on arrive à un résultat sympathique, qui répond à beaucoup d’anciens problèmes. Bien évidemment React va plus loin en permettant un rendu partiel du DOM modifié, comme Elm.
React apporte ainsi deux paradigmes importants dans le monde du JavaScript :
- Le raisonnement en composants, qui cache en réalité des fonctions pures.
- De l’immutabilité des états.
Le plus impressionnant dans cette approche novatrice, ce n’est pas tant qu’elle soit novatrice. Produire des idées nouvelles est plutôt simple. Produire des idées nouvelles qui ont une bonne expérience développeur est une tâche difficile et React le fait très bien. Après quelques minutes d’adaptation on comprend vite l’intêret d’une telle approche.
Pourtant je n’ai toujours pas parlé du plus bel apport à l’experience de React : le JSX. Avec ces petits exemples, on se rend vite compte qu’écrire des grandes parties d’interface va être laborieux et très différent de ce qu’on a l’habitude de faire. Déclarer du HTML dans une chaine de caractère, ce n’est pas très pratique.
Le JSX prend notre dernier exemple et le transforme en quelque chose de très lisible et de plus pratique :
const Button = () => {
// rest of the code ...
return (
<button onClick={increment()}>Cliqué {count} fois</button>
);
};
// mais surtout ensuite de faire
const App = () => {
return (
<Button />
);
};
React apporte un ensemble de pratiques qui modifient là façon dont on pense et écrit du JavaScript.
Des approches qui provquent le changement
Si l’on se penche un peu sur cette nouvelle syntaxe, tout ressemble de plus en plus à ce qu’on peut retrouver en programmation fonctionnelle.
const sayHello = (msg, name) => "Hello", + name + " , here my message :" + msg
say_hello : String -> String -> String
say_hello msg name = "Hello " ++ name ++ " , here my message : " ++ msg
Un this
moins imprédictible
ES6 répond à un point du “The JavaScript Problem” (1), celui de “verbose function syntax¹” mais règle aussi un autre souci, celui du comportement de this
.
Avant l’introduction de celels-ci, le comportement de this
en JavaScript était une source majeure de confusion. Cette confusion venait du fait que la valeur de this
dépendait de la manière dont une fonction était appelée, et non de l’endroit où elle était définie.
Prenons un objet avec une méthode :
const obj = {
name: "Test",
logName: function () {
console.log(this.name);
},
};
obj.logName(); // Affiche "Test"
Ici, this
fait référence à obj
, car logName
est appelée comme méthode de l’objet. Mais si on extrait la méthode et qu’on l’appelle indépendamment, tout change :
const extractedLogName = obj.logName;
extractedLogName(); // Erreur ou `undefined` selon le mode strict
Dans ce cas, this
n’est plus lié à l’objet obj
, mais au contexte global (window
dans un navigateur ou undefined
en mode strict). Cette flexibilité du lien de this
rendait JavaScript difficile à raisonner dans des contextes plus complexes.
Les arrow functions ont changé la donne en fixant la valeur de this
au contexte lexical où la fonction a été définie. Cela signifie que this
n’est jamais redéfini par l’appel de la fonction et conserve la valeur qu’il avait au moment de l’écriture du code.
Exemple simple avec une arrow function
const obj = {
name: "Test",
logName: () => {
console.log(this.name);
},
};
obj.logName(); // Affiche `undefined`
Contrairement à une fonction classique, ici this
ne fait pas référence à obj
mais au contexte parent (le contexte lexical). Comme la flèche ne crée pas son propre contexte de this
, elle utilise celui dans lequel elle a été écrite.
Dans le contexte d’un composant React
, on comprend tout de suite son intêret !
Au lieu de devoir exposer par exemple une function increment
au scope global, ce que je fais plus haut dans l’exemple, je peux définir à la volée pour un évenement, le onClick
ici, une arrow function
qui aura accès à mon état local !
// previous code of Button
const increment = () => {
setCount((prev) => prev + 1);
}
// Use a closure to bind the click event
const renderButton = () => {
const button = document.createElement("button");
button.textContent = `Cliqué ${count} fois`;
button.onclick = increment; // Pass the increment arrow func
return button.outerHTML;
};
return renderButton();
Si l’on pousse la réflexion un peu plus loin, les classes ont presque disparues alors qu’elles structurent une grande partie des projets back-end. Propre aux langages fonctionnels, la nouvelle pratique du JavaScript se pense autours des données et des fonctions.
Des fonctions transforment des données passées en entrée. Tout est une fonction qui transforme des données.
De l’immutabilité par défaut
En JavaScript, la seule manière d’avoir de l’immutabilité par défaut est de déclarer des constantes avec const
.
Si l’on observe le code aujourd’hui produit, c’est la principale façon de déclarer des variables. Alors oui, ce ne sont plus des données qui peuvent varier dans le temps mais reste qu’elles peuvent avoir un valeur inconnue par l’environnement d’execution à la première assignation. On retombe donc bien dans la notion d’immutabilité des données.
Vous pouvez me répondre qu’on peut expliquer autrement l’usage de const
: cela permet d’éviter d’écraser la définition d’une fonction portant le même nom dans le scope, comme c’est le cas avec les fonctions déclarées avec la méthode tradtionnelle.
Cela s’entend bien et je pense que c’est un facteur contribuant à cette approche, mais pas sa seule origine.
Avec React
, tous les états sont des données immutables et l’approche immutable est la référence.
Vers une nouvelle gestion des erreurs
Une proposition (3) en vue de changer le fonctionne de gestion des erreurs apporte aussi son lot de nouveautées.
Historiquement, une grande parties de langages orientés objet (mais pas seulement) proposent une gestion des erreurs que j’appelerais globale avec le try...catch
.
Ce modèle global est aujourd’hui remis en question par des paradigmes plus déclaratifs et l’approche fonctionnelle par la monade Result
.
Sans aller jusque là, la présente proposition prend le chemin du millieu, avec une destructurisation des résultats.
Popularisé par React dans l’univers JS, l’utilisation de la destructuration des résultats de fonctions, avec useState par exemple, on obtient des valeurs sous la forme :
const [a, setA] = useState('a');
Cette approche n’est pas une nouveauté en soi, elle fait écho à des pratiques récurrentes en programmation fonctionnelle où on renvoie des tuples ou des structures, afin d’exprimer de manière claire les résultats de fonction. Dans le cas d’un langage orienté vers les données plutôt que les types, une telle approche semble évidente.
C’est d’ailleurs déjà la manière dont Go gère les erreurs.
Les contraintes techniques associées à un usage croissant d’une nouvelle façon d’écrire du code produisent des changements des attentes nouvelles dans les spécifications du langages au point d’ajouter ces nouvelles fonctionnalités.
Est-ce que sans l’avénement de React
on aurait vu de tels changements? Il m’est d’avis que non.
The coolest thing about React is that it has brought the ideas of functional programming and immutability to a wide audience. Some people might not really like to admit that, but, you know, they might say, “Oh, you know, I never said I like functional programming or anything,” and that’s fine. I think we should meet people where they are, and I find that if you just call it declarative programming, it’s just an easier pill to swallow. It’s like they don’t want to question their identity as a programmer. 4
Quand React s’inspire d’Elm
Imaginons un instant une page qui contient des composants. Chacun de ses composants contient lui aussi d’autres composants. Maintenant donnons que l’application authentifie l’utilisateur et soit dans la nécessité de passer cet utilisateur à tous les componsants de dernier niveau.
Comment faites-vous cela ? Par injection de dépendance vous me dites ? Oui effectivement. Dans l’univers de React, on appelle ça du prop drilling (passer des props de façon descendante). On peut passer aux composants enfants un état de l’application, notre utilisateur par exemple.
Maintenant ajoutons beaucoup d’autres états passés ainsi en injection de dépendance. Que se passe-t-il ? C’est vite très complexe à gérer.A cette époque, React ne propose aucune solution ad-hoc pour gérer les états de manière centralisée comme pourrait le faire aujourd’hui la Context API.
C’est alors ici que Redux
rentre en jeu.
Redux est un gestionnaire d’état global, conçu pour JavaScript mais surtout React. Il permet de centraliser et de gérer l’état de l’application de manière prévisible, évitant ainsi les problèmes liés au prop drilling.
Redux fonctionne avec un “store” unique qui contient tout l’état de l’application. Au lieu de passer des props à travers de multiples niveaux de composants, les composants peuvent accéder directement au store, peu importe où ils se trouvent dans l’arborescence.
Il se décompose en 3 grands composants :
- Store : L’objet central qui contient l’état global de l’application.
- Actions : Les actions qui décrivent les changements à apporter à l’état.
- Reducers : Des fonctions pures qui prennent l’état actuel et une action, puis retournent un nouvel état.
Ce fonctionnement plusieurs avantages qui n’étaient pas présent jusqu’à présent avec React.
- On sépare la logique de gestion d’état de la déclaration des composants. On gagne en maintenaniblité de l’application.
- Un état global est immuatable garantit la cohérence de l’application.
- Ca permet de suivre toutes les actions et changements d’état, et donc de pouvoir debugger avec des retours en arrière. Il ne suffit que d’enregistrer l’historique du store et de le restaurer à une étape passée.
Ce dernier point est un élement très important dans le succès de Redux. Cette la première fois que l’on peut debugger une application avec autant de finesse en connaissant l’état de l’application à chaque moment, et avec chaque action réalisée. Une sorte de debugger par étape pour le front-end.
Redux ne présente que des avantages une fois présenté comme cela. Pourtant, son intêret premier est de répondre au prop drilling, mais est-ce vraiment un problème ?
A quel moment Redux devient utile ? Pour les développeurs qui sont habitués à l’injection de dépendance, il peut falloir beaucoup d’états à partager aux composants enfants, et c’est ce qu’en pense Jordan Walke lui-même :
It depends on the project, but I have a high tolerance for passing props down the hierarchy explicitly, so it takes me a while before I reach for something more involved 2
Personnellement, j’ai aussi une tolérance plutôt élevée, habitué au schéma pas seulement dans le front-end et React. Oui en fait je n’utilise pas vraiment React.
Une fois cela présenté, ne trouvez-vous pas que Redux ressemble fortement à ce que fait Elm ? J’entends bien que mon titre vous gachait totalement la surprise.
A l’instar d’Elm, l’état de l’application est centralisé et mis à jour dans une fonction prévue à cet effet. Redux le fait avec des reducers
, fonctions pures qui transforment un état et une action en un nouvel état.
C’est ni plus ni moins qu’un remake de la fonction update
d’Elm.
Regardons cela, avec l’exemple officiel de Redux sur les reducers :
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
default:
return state
}
}
Ce qui donnerait en Elm :
type Msg = TodoAdded String
type alias Model = { todos : List Todo }
type alias Todo = { id : Int, text : String, completed : Bool }
init : Model
init = { todos = [] }
update : Msg -> Model -> Model
update msg model =
case msg of
TodoAdded text ->
let
newId = List.length model.todos
newTodo = { id = newId, text = text, completed = False }
in
{ model | todos = model.todos ++ [newTodo] }
Redux modifie un état et non un model, suivant une action et non un message, mais hormis la sémantique, la logique reste la même.
L’inspiration n’est pas cachée, on retrouve même une section dédiée dans la documentation : https://redux.js.org/understanding/history-and-design/prior-art. Mais est-ce vraiment le cas ? Est-ce que Dan Abramov s’est directement inspiré d’Elm ? La réponse n’est pas si évidente.
I didn’t mean to create a Flux framework. When React Europe was first announced, I proposed a talk on “hot reloading and time travel” but to be honest I had no idea how to implement time travel. I thought about it for a while, and I knew there was prior art in Elm, so I read Elm Architecture, but forgot about it soon. 5
Son inspiration première pourrait être attribué à Flux. Mais creusons plus loin :
Then Andrew Clark suggested we just combine reducer functions into a single reducer function. This would kill the need for dispatcher. Just like UI component tree has one component at the root, different reducer functions can be called from a single root reducer function. Later I realized that this is exactly Elm architecture, and I just didn’t understand it at first. 5
Ah ! Elm ! Enfin ! Cette courte citation nous éclaire sur un point crucial, même pour un développeur du niveau de Dan Abramov, la Elm Architecture n’était pas si évidente, par son changement de paradigme. Pour la comprendre, il lui a fallut un tour complet, réinventer la roue et l’adapter dans un contexte JavaScript.
Peut-on pour autant dire qu’Elm ne fait pas parti de l’équation ? Sans les travaux d’Evan Czaplicki la solution n’aurait peut-être pas paru si évidente. Faut-il voir une filiation directe entre Redux et Elm ? Evidemment que non, mais l’on peut imaginer un lien par la force des choses, par le mouvement des idées.
Je pense qu’il est temps de conclure cette partie sur une autre citation de cet interview, qui me fait sourrire. Parfois des tentatives que l’on pense insignifiantes changent à jamais un écosystème.
This is how Redux came to be. I was trying to write some code for a fancy React Europe demo, but it turned out that people really liked the architecture, so I marketed it as a library, although there is really very little code there. 5
TypeScript, une réponse un peu fonctionnelle aux problèmes de JavaScript
Dans la lignée des transpileurs vers JavaScript, TypeScript est le plus grand succès que l’on connaisse au point où JavaScript est synomyme de TypeScript. Il est difficilement concevable de ne faire que du JS pur de nos jours.
La réponse qu’apporte TS au “The JavaScript Problem” 1 est satisfaisante pour la plupart des développeurs et des cas d’usage. Je ne souhaite pas reprendre point par point ces réponses, et je souhaite surtout souligner les influences du monde fonctionnel à TS.
Oui parce que l’apport évident de TS est en premier son typage statique optionel.
Il est très facile déclarer de nouveaux types en TS ainsi que de typer toutes les données. C’est bien souvent le premier point mis en avant à TS : JavaScript avec des types, et je crois que son nom est plutôt parlant.
Quand TS n’arrive pas à trouver le type d’une donnée, il va implicitement avoir recours à any
, ce qui devrait poser problème. Mais par défaut TS propose d’interdire ce cas. On tombe donc sur une bonne solution.
Mais il y a type et type. Son apport novateur dans le monde des langages mainstream c’est le typage structurel. Cela permet de comparer une égalité de type à sa structure.
type Animal = {
name: string,
age: number,
}
type Human = {
name: string,
age: number,
another_field: string,
}
type Cat = {
name: string,
age: number,
human: Human,
}
const me: Human = {
name: "Cédric",
age: 30,
another_field: "Je ne sais pas"
}
const theCat: Cat = {
name: "Awesome cat",
age: 5,
human: me,
}
// TypeScript va destructurer le type Animal et extraire seulement name de type String
// Tout type qui a un champs name de type string et age de type number sera accepté
const sayHello = ({name}: Animal) : string => {
return `Hello ${name}`;
}
console.log(sayHello(me));
// Hello Cédric
console.log(sayHello(theCat));
// Hello Awesome cat
console.log(sayHello({name: "Eh", age: 100}));
// Hello Eh, fonctionne car structurellement égal à Animal
console.log(sayHello({name: "Eh"}));
// Ne fonctionne pas, car pas structurellement égal à Animal, il manque `age`
Le typage structurel est plutôt récurrent dans la programmation fonctionnelle parce qu’il permet de se concentrer sur les données et leur appliquer des transformation. Contrairement au typage nominal où les types sont des contraintes, ici les types sont des outils.
On voit aussi apparaitre une immutabilité plus forte qu’en JS, avec la possibilité de géler (freeze) les propriétes au sein d’un objet et tableau, qui rend les données vraiment immutables. En JS il est toujours possible de les modifier après déclaration via const
.
Ces apports ne sont pas suffisants pour les partisans d’une programmation fonctionnelle pure (sans jeu de mot), en effet TS offre tout de même un type null
et n’apporte pas de type algébriques. Pour autant toutes les alternatives qui le font n’ont pas eu son succès.
Je pense que son succès réside dans son compromis parfait pour la plupart de la communauté JS.
Une autre génération de concurrents à TS
Vous n’avez pas oublié Jordan Walke j’espère, le créateur initial de React.
Pour lui l’écosystème JavaScript n’est pas suffisant pour vivre une bonne expèrence de développement. C’est alors que ReasonML né. Vous reconnaissez le suffix ML ? C’est l’indication d’un langage de la famille ML, et plus précisement d’une syntaxe supplémentaire pour OCaml.
ReasonML is a new language toolchain that we are working on within a small group of open source community members - many of then being members of the React community. Reason provides a friendlier, more familiar interface to the OCaml compiler, which is a great, statically typed, high-performance compiler. We enjoy compiling to JavaScript via BuckleScript (and sometimes jsoo) and also target native binaries. 1 ReasonML addresses the biggest problems that I’ve observed in building UI applications over the last five years, and opens up a language/compiler toolchain that is incredibly well suited to React’s model of rendering UI [..] 1
ReasonML est plus profondement lié à React qu’on peut le penser. Vous vous souvenez des cours de ML qui a inspiré React, et de sa motivation première à concevoir React ?
my personal goal is to build an application that is fast and that doesn’t break when it’s extended and I keep finding that it’s just way way too hard 4
React a pu faire ce qu’une librairie permet : simplifier la création d’UIs avec un modèle d’immutabilité. Mais elle ne résout pas le problème du typage statique, des types avancées, du l’immutabilité par défaut, etc.
Reason (1) brings what people like about react to the programming language level 4 (1) ReasonML
Lorsqu’on regarde React plus globalement, on se rend compte que son approche n’est pas très pratique avec JavaScript et c’est le point de tous les paragraphes jusqu’à présent. Je l’ai dis, il est surtout vu comme un véhicule pour heberger autre chose, parce que c’est le dénominateur commun le plus évident pour le web. C’est un élement que l’on retrouve dans le discours de Jordan Walke, JavaScript permet de réduire ce qu’il appelle la “static friction”, l’inertie au démarrage. React n’aurait jamais pris sans JS, et donc ces idées n’auraient jamais pris. C’est le constat d’échec d’Elm en tant que langage.
Une fois cette première friction passée, peut-être que proposer un langage qui apporte à JS tout ce que React n’a pas pu et la seconde étape. Mais pour cela, il reste des frictions. Tout le monde n’aime pas les langages fonctionnels, ils apportent beaucoup de friction en supprimant les boucles, les valeurs mutables, un typage statique, etc…
Le projet de ReasonML est de proposer un langage qui réduit ces frictions au maximum par une syntaxe qui plait, tout comme React l’a fait.
Presse papier et documentation
Idées à noter:
- Le code comme littérature et pratique sociale. La façon dont on code se construit avec le temps, les langages s’hydrident.
==> Le style de code en closure en js au lieu de fonctions traditionnelles, issu des lambdas
\var -> var ++ var
Docs
- Js_of_Ocaml : premier commit Feb 1 2010 (https://github.com/ocsigen/js_of_ocaml/commit/3f5b507783df64b080dc4da5113d5c7d601cafcf)
- Itw de Jordan Walke en 2017 : https://www.reactiflux.com/transcripts/jordan-walke
To finally answer your question: yes React was inspired by many other technologies including other UI frameworks which we had been using at the time. More than anything, React was inspired by the ML family of languages (including SML/OCaml) which helped me articulate the value (no pun intended) of immutability. but ultimately ideas are cheap, and creating an initial version of something is definitely the easiest part. The “idea” of React by itself, doesn’t explain why React has become the phenomenon that it has. I believe the culture and energy of the React community is the reason for React’s massive success.
- The Javascript Problem History : https://wiki.haskell.org/index.php?title=The_JavaScript_Problem&action=history Première modification/création : 27 janv 2012 ==> Il faudrait faire une étude des changements sur cette page