Marier React et Symfony

Cet article fait suite à notre conférence « Comment marier Symfony et React ? » présentée au PHP Tour 2017.

On ne présente plus React, framework JavaScript bien connu et à la pointe de la mode depuis 2 ans déjà. Les tutoriels / articles pour créer votre projet en React ne manquent pas sur les Internets : on crée une API, puis un front full React pour la consommer. Ce n’est donc pas cette approche que nous vous proposons de discuter ici mais plutôt comment utiliser React dans un projet Symfony existant, sans nécessairement découpler les deux.

Pour ce faire, nous retenons deux solutions :

Les deux méthodes sont similaires sur beaucoup d’aspects, la qualité principale de LimeniusReactBundle sera de vous permettre de mettre facilement en place le rendu serveur de vos composants, ainsi que d’avoir accès à des extensions Twig pour faciliter l’intégration de React dans vos templates.

Mais commençons par le commencement. React, on ne le présente plus certes, mais chacun n’est pas familier pour autant avec ses concepts de base. Ceux-ci peuvent se résumer (grosso modo) en trois points :

  • des composants écrits en JSX ;
  • qui ont des props et un state ;
  • un algorithme de réconciliation.

Si vous connaissez déjà React, aller directement à la partie React et Symfony.

Pour la suite de cet article, nous allons choisir un exemple simple : la réalisation d’une todo list. Les spécifications sont les suivantes : afficher une liste de choses à faire ; pouvoir marquer un élément de la liste comme « fait » afin qu’il disparaisse de la liste ; le tout dans une page déjà écrite avec Twig ; avec des données (éléments de la liste) provenant d’un contrôleur Symfony.

React en bref

Grands principes : réconciliation et DOM virtuel

Pour agir sur l’affichage dans le navigateur, React utilise un système de DOM virtuel, combiné à un algorithme de différenciation : la réconciliation. Concrètement, chaque élément du DOM ainsi que son état dans le DOM physique du navigateur est répercuté dans un DOM virtuel construit par React. Voici un schéma explicatif : React DOM

Quand une modification survient, du côté du navigateur (l’utilisateur clique sur un bouton (1)) cela déclenche un événement (2) que React écoute et utilise son algorithme pour calculer la différence entre le DOM physique et le DOM virtuel (3). Il génère ainsi un certain nombre d’opérations à effectuer pour réconcilier ces deux états, les synchroniser, puis applique (4) ces modifications grâce à l’API DOM du navigateur.

Une modification peut survenir directement du côté de React (un appel à une API renvoie une liste d’éléments qui sera affichée), dans ce cas on est directement dans l’étape (3).

À savoir : Alors que l’algorithme de réconciliation est partie intégrante de la librairie React, le DOM virtuel ne provient pas à proprement parler de React mais de ReactDOM. Dans le cas de React Native, on utilise d’autres méthodes pour appliquer les modifications générées par l’algo, puisqu’une application mobile ne fonctionne pas grâce à un DOM. Le principe reste cependant le même.

Les composants

Une application React peut être représentée par un ensemble de composants imbriqués les uns dans les autres. L’intérêt majeur de cette architecture est qu’elle permet d’avoir des « morceaux de code » réutilisables. On va donc écrire nos composants en se basant sur un postulat simple : une responsabilité (même mineure) = un composant.

Reprenons notre exemple, on veut afficher une liste de choses à faire, avec un bouton permettant de marquer un élément comme fait. On aura donc trois composants :

  • le composant parent qui représente la liste de choses à faire (TodoList) ;
  • le composant qui représente une chose à faire (Todo) ;
  • le composant qui représente le bouton d’action (TodoButton).

Chacun de ces composants implémente la méthode render() qui définit le rendu de ce composant. Ce rendu peut-être constitué de code HTML (ou plutôt pseudo-HTML, grâce à JSX) et/ou d’autres composants imbriqués.

class TodoList extends Component {
  render() {
    return (
      <div className="well">
        {/* code du rendu */}
      </div>
    );
  }
}

Le rendu d’un composant à un instant t dépend de deux sources de données : les props et le state.

Les props et le state

On peut comparer un composant à une fonction, qui reçoit des paramètres (les props) et manipule également des variables locales (le state).

Les props sont passées à un composant soit par le composant appelant (<Todo item="Acheter du lait" done={false} />), soit par des décorateurs (injectIntl par exemple, proposé par react-intl et qui permet d’injecter la prop intl, de manière à gérer des traductions, dans un composant). On parle ici de flux de données unidirectionnel : les props sont toujours transmises du parent vers l’enfant et jamais en sens inverse.

Le state quant-à-lui représente l’état d’un composant, il est défini à l’intérieur du composant lui-même, qui en a alors la responsabilité.


class Todo extends Component {
  constructor() {
    super();

    this.state = {
      visible: true,
    };
  }

  markAsDone() {
    this.setState({
      visible: false,
    });
  }

  render() {
    if (!this.state.visible) {
      return null;
    }

    return (
      <li>
        <span>{this.props.item}</span>
        <button onClick={this.markAsDone.bind(this)}>
            Fait
        </button>
      </li>
    );
  }
}

Cette introduction à React est volontairement concise, et nous vous invitons pour aller plus loin à vous référer à la documentation existante, très bien fournie. Une liste de références intéressantes est à votre disposition à la fin de l’article.

React et Symfony

De plus en plus de projets utilisent conjointement React et Symfony. Il existe deux manières principales de marier ces deux technologies, que nous allons étudier ensemble.

React se nourrit de votre API

Si vous disposez d’une API existante, c’est certainement la solution la plus adaptée, peu importe d’ailleurs que votre API utilise Symfony ou non.

Comme dit plus haut, nous ne détaillerons pas ici la mise en place de cette architecture. Vous trouverez plusieurs liens vers des articles détaillant cette architecture et sa mise en place à la fin de l’article, si c’est cette solution qui vous intéresse.

En revanche, nous avons jugé intéressant de mettre l’accent sur quelques détails d’implémentations, basés sur notre expérience personnelle.

Logique métier

Dans des projets avec une logique métier forte, séparez au maximum les responsabilités pour éviter d’avoir à dupliquer la logique métier.

Dans le cas d’un site d’e-commerce avec workflow de commandes complexes par exemple, vous pouvez pour chaque commande, passer dans votre API son état et les transitions possibles. Ainsi, côté React vous aurez juste à créer des boutons permettant d’appliquer ces transitions, sans vous soucier de leur signification, et donc sans dupliquer la logique métier.

De la même manière, évitez de redéfinir des énumérations ou des listes de constantes, il est très rapide de mettre en place une méthode d’API qui retourne la liste de chacune des constantes, d’appeler cette méthode à chaque premier chargement de votre site React, puis de stocker ces valeurs dans un store global grâce à redux par exemple. Vous y aurez ainsi accès n’importe où dans vos composants et cela vous permettra d’avoir un code plus facilement maintenable.

Ce principe ne vaut pas pour tous les cas évidemment, la validation d’un formulaire, même si elle est complexe, devra quand même être implémentée des deux côtés (du moins si vous souhaitez des formulaires agréables à utiliser).

Internationalisation

Dans le cas d’un site multi-langues, il existe deux stratégies principales :

Tout traduire côté API

Vos messages quels qu’ils soient sont renvoyés traduits dans toutes les langues supportées et prêts à être affichés. Dans les faits cela peut poser problème puisqu’il devient impossible de changer le wording côté React. On peut également mettre en place un mécanisme pour préciser à l’API la langue souhaitée (avec un header Accept-Language dans vos requêtes par exemple), afin de récupérer seulement les traductions dans la ou les langues souhaitées.

Passer des clés de traductions via l’API, et traduire côté React

Simple à mettre en place côté API, il faudra cependant faire attention aux exceptions et aux erreurs de validations qui peuvent avoir des messages par défaut. Côté React, il faudra également implémenter un mécanisme pour réaliser ces traductions (par exemple en utilisant react-intl), et déporter tout le catalogue de message côté React, ce qui peut s’avérer plus gourmand en ressources. Il est possible (de la même manière que pour les constantes), de conserver le catalogue de messages côté API, et de le récupérer via un appel HTTP, mais on retrouvera alors les mêmes problèmes que pour la solution précédente.

Aucune de ces solutions n’est meilleure que l’autre, il faudra juste choisir la plus adaptée à votre cas et surtout vous y tenir sur tous les aspects du projet (messages d’erreur, erreurs de validation, etc…).

Librairies existantes

Si vous souhaitez réaliser un back-office avec cette architecture (un cas très fréquent et bien documenté), il existe de nombreuses technologies « clé en main » qui permettent de réaliser un back-office en React avec une API Symfony.

Parmi celles-ci on peut citer admin-on-rest, qui permet de réaliser très rapidement une interface d’administration basée sur une API existante et quel que soit le format utilisé par cette API. Il vous faudra simplement créer un « client », qui prend en paramètre le type d’action que vous souhaitez effectuer (GET_ONE, GET_LIST, CREATE…), la ressource (article, commentaire…), et un ensemble de paramètres (une représentation json d’un objet à créer par exemple) et qui exécute la requête adéquate et retourne une promesse. restClient(GET_ONE, 'posts', { id: 123 }) Il est également possible très facilement de personnaliser votre back-office.

Si vous utilisez api-platform pour votre API, vous pouvez également mettre en place un back-office en utilisant api-platform-admin. Cette librairie, basée sur admin-on-rest, permet de créer très rapidement le back-office correspondant à votre API (en parsant la documentation de l’API pour déterminer seul les routes existantes). La librairie a l’avantage d’être extrêmement rapide à mettre en place et on a la possibilité de le personnaliser complètement.

React embarqué dans Symfony

Si vous souhaitez utiliser React sur une partie de votre site seulement, il est possible d’intégrer vos composants React directement dans Twig.

Reprenons notre exemple de la Todo List, nous avons donc nos répertoires components contenant nos composants et js contenant nos autres scripts et nos composants principaux dont le composant TodoList. Lors du build, webpack (ou un autre task runner) va récupérer uniquement les composants enfants nécessaires (compris dans TodoList) ainsi que index.js (on va y revenir un peu plus loin) et les compiler en un seul fichier : todolist.js. On peut le résumer avec le schéma suivant : Todo list component

Ce fichier js compilé va ensuite être inclus dans notre template twig avec la balise <script> habituelle :

{# layout.html.twig #}
{% block body %}
    <div
        id="todolist"
        data-items="{{ items|json_encode }}"
    ></div>
{% endblock %}

{% block javascripts %}
    <script type="text/javascript" src="{{ asset('todo-list.js') }}"></script>
{% endblock %}

Vous noterez la présence de la div #todolist et l’attribut de données data-items contenant la donnée items encodée en json (passée par le Controller).

Revenons sur le fichier index.js, dans celui-ci nous allons créer notre Composant TodoList en lui passant la props items qu’on a récupéré de la data-items avec la fonction getData. Enfin, on injecte ce composant au niveau de notre div #todolist.

// index.js

import React from 'react';
import ReactDOM from 'react-dom';

import TodoList from './TodoList.jsx';

const TodoListElement = document.querySelector('#todo-list');
const getData = (name, json = true) => {
    const value = TodoListElement.getAttribute(`data-${name}`);
    return json ? JSON.parse(value) : value;
};

const element = React.createElement(TodoList, {
    items: getData(items),
});

ReactDOM.render(element, document.getElementById('todo-list'));

Nous avons donc notre composant React dans notre fichier Twig.

On part donc du postulat que le client de votre API (ici le navigateur) est capable d’interpréter du JavaScript, de manière fiable et performante, ce qui n’est pas toujours le cas ! On peut prendre l’exemple des crawlers qui parcourent vos pages Web pour indexer leur contenu dans les moteurs de recherche. Certains d’entre eux sont (encore à l’heure actuelle) incapable de comprendre votre code JavaScript et ne verront donc que des pages vides. Cas de figure plus fréquent, les utilisateurs qui consultent votre site via des appareils très peu puissants (smartphones de premières générations, ou même le vieil ordinateur de bureau de votre grand-mère), risquent de voir les performances du site fortement dégradés.

Exemple de site vu par un robot qui n’indexe pas du javascript :

Avec JavaScript Bienici
Sans JavaScript Bienici

C’est là que le rendu serveur intervient.

Le rendu serveur

Avant de détailler notre dernière solution, il convient de parler un peu rendu serveur.

Kesako ?

Le rendu serveur, c’est donner à votre site la faculté de rendre les composants React… côté serveur. Concrètement, dans une application React classique, le serveur envoie à votre navigateur votre code React (transpilé en JavaScript et souvent compilé en un fichier), et c’est le navigateur qui va interpréter ce code, exécuter React, rendre vos composants, réagir aux actions de l’utilisateur, etc.

Le rendu serveur vise donc à pallier les problèmes de référencement, de performance et d’affichage de contenu pour tous les utilisateurs. Au lieu de renvoyer au navigateur votre code React, et de le laisser se débrouiller avec, le serveur génère lui-même le rendu HTML de vos composants et le renvoie au navigateur.

Plusieurs technologies existent pour faire du rendu serveur :

  • un serveur node externe ;
  • l’extension PHP V8JS ;

Nous avons aussi la librairie PhpExecJS qui détecte automatiquement le moyen à utiliser pour exécuter le JS (V8JS ou Node). Pour en savoir plus sur ces technologies, nous vous renvoyons aux slides de notre conférence.

Rendu client, serveur ou les deux

Le problème de ce type de rendu est que votre code React devient invisible pour votre navigateur. Celui-ci n’exécute aucun code JavaScript, et vous perdrez toute l’interactivité et la fluidité offerte par React. Le bouton qui faisait apparaître votre beau formulaire ne fonctionnera plus, de même que les filtres et les tris dans vos listes. Il faudra donc mettre en place des mécanismes alternatifs.

Le mécanisme le plus souvent utilisé est de remplacer chaque interaction de votre code React par un lien, permettant de charger une nouvelle page avec les informations à jour. Le bouton « Ajouter » qui ouvrait votre formulaire devient un lien vers une page de création, le bouton « Trier par date » un lien avec la même URL que votre page courante, mais avec un paramètre précisant le tri souhaité, et ainsi de suite. Cette solution vous permet de conserver une certaine interactivité avec un rendu full-serveur.

Comment ça full-server ? On peut faire du half-server ?

Eh bien oui, c’est possible, on pourrait donc profiter de tous les avantages du rendu serveur (premier affichage plus rapide, page lisible par les robots, affichage de contenu pour tous, même sans JavaScript…) tout en conservant le dynamisme offert par React côte client. Il est donc extrêmement puissant sur le papier, mais pas forcément évident à mettre en place, et c’est là qu’intervient Limenius ReactBundle.

LimeniusReactBundle

La dernière solution que nous vous proposons est donc d’utiliser un bundle. Il en existe plusieurs à l’heure où nous écrivons, mais le seul à vraiment sortir du lot est Limenius ReactBundle.

Basé sur React-on-Rails, ce bundle permet d’appeler vos composants React directement dans Twig, en exposant une extension Twig react_component. Il utilise PhpExecJS pour détecter l’environnement JavaScript installé sur vos serveurs (entre V8JS et Node) et utilise celui-ci. Il définit également une extension permettant de gérer un store pour les projets utilisant redux. Enfin, il permet de préciser, via un simple paramètre, si vous souhaitez du rendu client uniquement, serveur uniquement ou les deux.

{{ react_component('HomePage', {'rendering': 'server_side'}) }}
{{ react_component('HomePage', {'rendering': 'client_side'}) }}
{{ react_component('HomePage', {'rendering': 'both'}) }}

On a donc :

  • server_side : rendu du composant uniquement côté serveur.
  • client_side : rendu du composant uniquement côté client.
  • both : rendu du composant client ET serveur.

Pour ce dernier choix, nous pouvons imaginer une page qui fonctionne de la manière suivante :

  • Le navigateur appelle votre serveur et lui demande d’afficher une liste d’utilisateurs.
  • Le serveur interprète le code React et renvoie au client le HTML généré ET le code React.
  • Le navigateur affiche le code HTML sans se poser de question puis commence à interpréter le code JavaScript.
  • React, grâce à une variable JavaScript que le serveur a gentiment glissée dans le code généré, reconnaît le code HTML présent comme issu de vos composants.
  • Il reconstruit un DOM virtuel basé sur ce que vous voyez à l’écran, sans toucher au HTML, et se « réapproprie » le code.
  • React ayant repris la main sur l’affichage, vos boutons, filtres et autres joyeusetés reprennent le fonctionnement attendu.

Ce bundle nous permet donc d’écrire librement nos composants et d’appliquer un rendu serveur (ou non) uniquement sur les composants nécessaires. Le rendu serveur est utile mais assez coûteux niveau performance, l’ajout d’une feature-flag pour désactiver le rendu serveur peut être utile lors de très fortes affluences sur le site.

Vous pouvez voir les slides et la vidéo de notre conférence.

Références et bibliographie

Vous trouverez ici de la documentation et des articles intéressants pour approfondir sur ce sujet :

Nos formations sur le sujet

  • Symfony avancée

    Décou­vrez les fonc­tion­na­li­tés et concepts avan­cés de Symfo­ny

  • React

    Développez des interfaces efficaces et élégantes avec la bibliothèque Javascript React.

blog comments powered by Disqus