Thibault Lenclos
Développeur web et mobile chez Jolicode, Symfony 💛 React+native.
Il existe quelques ressources montrant comment associer Symfony et React dans un projet, et je souhaite aller plus en détails sur le démarrage d'un projet de ce style ainsi que les différentes complications qu'il est possible de rencontrer.
Je vous invite à aller lire comme première partie l'article "Marier React et Symfony" qui vous donnera une bonne introduction à React et des premières réponses concernant l'internationalisation, la communication entre Twig et React ainsi que la mise en place du rendu serveur.
Vous trouverez dans la suite des recommendations et bonnes pratiques qui viennent compléter l'article pour une bonne intégration de React dans votre projet Symfony.
Une bonne pratique de développement front est d'utiliser un module bundler pour regrouper ses assets ainsi que rajouter des transformations à notre code. La communauté React s'est orientée principalement vers Webpack et c'est aussi le cas de Symfony.
La configuration de ce dernier peut s'avérer un peu verbeuse et obscure, il est donc possible d'utiliser Webpack Encore qui est une légère couche d'abstraction avec de la configuration prédéfinie qui s'associe parfaitement avec la configuration du framework.
Les principes de Encore sont les mêmes que pour Webpack : des points d'entrées, un dossier de destinations, des loaders... Si vous connaissez déjà l'outil vous ne serez pas perdu.
Pour notre application il est nécessaire d'activer les presets React comme expliqué ici https://symfony.com/doc/current/frontend/encore/reactjs.html
Grâce à Webpack Encore et sa configuration par défaut, nous pouvons utiliser la syntaxe ES6 qui est le standard actuel de JavaScript. De plus en activant le preset React nous débloquons aussi la syntaxe JSX utilisée par les composants..
Le site web babeljs.io est très pratique pour se familiariser avec cette syntaxe, il est aussi possible d'utiliser le REPL et d'expérimenter avec le code généré.
// Avant
const helloWorldJsx = (
<div>
<p>Hello there !</p>
</div>
);
// Après
var helloWorldJsx = React.createElement(
"div",
null,
React.createElement(
"p",
null,
"Hello there !"
)
);
Il y a plusieurs approches selon si vous rajoutez React sur de l'existant ou sur un projet de zéro. En effet si c'est dans une optique de migration il va être plus intéressant de démarrer par quelques composants simples tel qu'un autocomplete ou un datepicker avant de refactoriser une interface de recherche complète.
Amélioration ou migration de l'existant: à privilégier lors de l'intégration de React sur de l'existant ou quand vous souhaitez garder le maximum d'interface dans Twig. Démarrez par quelques éléments de l'interface non critique pour se familiariser avec le paradigme des composants et de la communication entre ces derniers. Il est préférable d'aborder une nouvelle technologie par itération plutôt que refactoriser tout le front et se retrouver coincé dans de mauvaises implémentations.
Application JavasScript: Cette approche est à envisager lorsque vous partez de zéro et souhaitez avoir une application complètement JavaScript. Ainsi Symfony sera principalement utilisé comme serveur d'API. Sur ce type d'application il y aura des concepts beaucoup plus avancés tels que le routing côté client, l'authentification ou encore le rendu serveur avec Node.js.
Vous allez surement avoir besoin de passer des données venant de votre contrôleur à votre composant. L'approche la plus simple étant d'utiliser Twig comme passerelle pour vos données. Soit en utilisant les data-attributes
{# layout.html.twig #}
{% block body %}
<div id="list" data-items="{{ items|json_encode }}"></div>
{% endblock %}
ou en utilisant des helpers d'initialisation de vos composants, exposés globalement
{% block JavaScripts %}
<script type="text/javascript">
MyApp.initListComponent('todolist', {{(items|json_encode|raw)}})
</script>
{% endblock %}
J'ai tendance à préférer la deuxième approche qui me permet de savoir quel composant est utilisé directement dans ce template Twig, celà évite aussi de polluer le DOM et économise un querySelector en plus pour récupérer les données.
Il est bien sur aussi possible de récupérer des données en exécutant une requête Ajax à Symfony. Par exemple sur un site avec un proxy de cache HTML, il est impossible d'avoir des informations spécifique à l'utilisateur dans le html généré par Twig. Une requête Ajax pour récupérer les informations de l'utilisateur est donc envoyé au chargement de la page puis le résultat mis en cache dans le local storage. Nous verrons plus tard que ce mécanisme est facile à implémenter via Redux.
Cela marche dans l'autre sens, pour envoyer vos données à l'applicatif PHP une simple requête HTTP suffit.
fetch('https://api.github.com/gists', {
method: 'post',
body: JSON.stringify(opts),
// Inclure les cookies dans la requête
credentials: 'include',
// Pour que vos contrôleurs Symfony considèrent la requête comme une requête Ajax
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).then(response => response.json())
Vous trouverez de nombreux articles concernant les bonnes pratiques, en voici une liste non exhaustive
https://www.robinwieruch.de/tips-to-learn-react-redux/ (en particulier la partie "On-boarding" si votre équipe démarre)
https://engineering.musefind.com/our-best-practices-for-writing-react-components-dec3eb5c3fc8
C'est l'architecture autour de React la plus populaire. Elle permet de centraliser les données tout en permettant un partage facile de celles-ci dans vos composants. On pourrait résumer simplement via cette formule :
(previousState, action) => newState
Notre application va donc contenir un état ("state", à ne pas confondre avec le state de vos composants) qui va évoluer via le traitement d'action. Les actions peuvent être des actions utilisateurs tels que le clique sur un lien mais aussi des effets de bords comme par exemple l'arrivée d'une réponse à traiter.
Les termes à comprendre sont "reducers", "action", "store" et "connect". Nous n'allons pas détailler Redux ici, la documentation officielle est déjà une très bonne référence.
Un cas pratique dans Symfony est quand vous disposez de plusieurs points de montages différents, par exemple un composant dans le header et un autre dans le footer, leur injecter le store Redux permet de communiquer facilement entre eux en partageant le même état.
Des middlewares peuvent apporter des fonctionnalités supplémentaires de manière très simple, comme par exemple redux-persist qui permet d'ajouter la persistance du state dans le local storage automatiquement.
Vous entendrez souvent ces termes lorsqu'on parle de composants containers. Dans Symfony nous avons l'habitude de rajouter des fonctionnalités via l'extension de classe PHP, dans React c'est la composition qui est privilégiée et on va ainsi parler de Higher-Order components (HOC). Le principe est de créer une fonction qui prend en paramètre un composant et va retourner un autre composant avec les nouvelles fonctionnalités. Ce pattern est devenu tellement commun qu'il est maintenant inclu dans la documentation officielle.
Il existe d'autres patterns et surtout des anti-patterns dans lesquels il est facile de tomber lors que l'on démarre ! N'hésitez pas à aller faire un tour sur le dépôt react-bits qui en liste quelques un, même en ayant utilisé React depuis sa sortie on y trouve de nouvelles idées.
Dans l'écosystème Symfony nous sommes habitués à chercher les bons outils via Packagist ou Github. Il en est de même pour JavaScript avec NPM qui va vous donner une liste classé par pertinence avec des statistiques sur les téléchargements et la maintenance du projet. Exemple pour chercher un composant de sélection de date : https://www.npmjs.com/search?q=react%20datepicker&page=1&ranking=optimal
Il existe aussi des sites pré-sélectionnant des composants de qualité comme le site JS.coach : https://js.coach/?search=react+datepicker&collection=React
L'intérêt du rendu serveur est de réduire le temps d'affichage de votre page en effectuant le rendu du HTML côté serveur. C'est ce qui est fait dans vos templates PHP naturellement et qui va être plus complexe à mettre en place avec vos composants React.
En effet l'environnement n'est pas le même, comme expliqué dans l'article de l'introduction, il est possible d'utiliser React avec le bundle LimeniusReactBundle dans Symfony. Il va vous apporter 2 manières de rendre vos composants, soit via V8js ou NodeJS.
Ce bundle va vous exposer des fonctions pour rendre un composant avec ses props dans vos templates Twig. Attention cependant cette architecture reste complexe (V8JS dans Php ou communication socket sur un process node) et les erreurs rencontrées peuvent être obscure.
Celle que vous risquez de rencontrer le plus souvent est le légendaire "window is not defined", en effet votre JavaScript n'est pas exécuté dans le navigateur et cet objet global n'existe pas, il faudra donc ruser si vous souhaitez importer du code qui nécessite cet objet. Si c'est simplement un import il suffira de détecter si l'objet window est défini, sinon nous possédons la fonction componentDidMount
dans un composant React.
// Vérification simple avant l'import d'un module qui nécessite l'objet window
if (typeof window !== `undefined`) {
const module = require("module");
}
// Utilisation des fonctions lifecycles de React, componentDidMount n'est appelé que sur le client
export default class MyClass extends React.Component {
...
componentDidMount() {
const MyWindowDependentLibrary = require( 'path/to/library' );
MyWindowDependentLibrary.doWork();
}
...
}
Le bundle cité plus haut se base sur React on Rails qui est une intégration de React avec le framework Ruby on Rails. Il existe aussi une autre alternative un peu plus récente et moins couplé à l'écosystème Ruby appelé Hypernova.
Si votre choix s'est posé sur une application Symfony comme API et une application React découplée, vos composants vont récupérer leur données via des requêtes HTTP au lieu d'avoir des données passées via Twig.
J'apprécie plus particulièrement cette approche car il est plus facile de faire évoluer indépendamment le front et le back. Cependant il faudra se pencher sur le rendu serveur ainsi que le routing d'une application NodeJS. Vous trouverez beaucoup, si ce n'est trop d'articles pour implémenter ceci. Avoir une intégration plus complète en mode single page app va vous permettre d'avoir des optimisations tels que du code splitting par rapport au routeur.
Il y a aussi de belles initiatives pour simplifier cette tâche comme le framework next.js.
Nous avons l'habitude d'effectuer des tests fonctionnels dans nos applications Symfony, pour vérifier nos réponses ainsi que la présence de certains éléments dans la page. Avec React et son approche composant nous pouvons avoir une approche plus unitaire pour le front.
Je recommande fortemment d'utiliser Jest (aussi développé par Facebook) qui va nous apporter une API pour manipuler les props de nos composants ainsi qu'une fonctionnalité de snapshot pour comparer des rendus.
Exemple d'un composant testé avec Jest
// Link.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link';
test('Link changes the class when hovered', () => {
const component = renderer.create(
<Link page="http://www.facebook.com">Facebook</Link>,
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
tree.props.onMouseEnter();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
tree.props.onMouseLeave();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
On peut aller beaucoup plus loin en rajoutant des tests de l'architecture Redux, encore une fois cette partie est elle aussi parfaitement expliquée dans la documentation officielle.
De la même manière qu'avec Symfony et les services par exemples, si votre code devient difficile à tester c'est qu'il est trop couplé et possède trop de responsabilité. Par exemple un composant responsable à la fois de récupérer des données sur une API, rendre du contenu et le rendre triable va être très difficile à tester et réutiliser, il faut donc le découper en 3 différents composants.
Suivre ces pratiques vous aidera à réaliser un projet de qualité avec du code réutilisable, testé et découplé. Il existe de nombreux starters kit avec React mais qui sont vraiment centré sur l'écosystème JavaScript. L''exemple open source le plus avancé avec Symfony est celui de LimeniusReactbundle avec cette sandbox dans laquelle vous allez pouvoir trouver une implémentation avec Redux. Il existe aussi d'autres intégrations React intéressante tels que le client generator d'API-platform pour créer rapidement un projet React avec une API.
À vos composants ! ⚛️ ⚛️️