19 mars 2020
Partagez votre code JavaScript entre vos applications React Native, React et Symfony
6 minutes de lecture
Vous êtes-vous déjà demandé s'il était possible de partager votre code JavaScript entre votre application mobile React Native et votre application web Symfony + React ? Nous oui, et nous avons pris le temps de répondre à cette question.
Notre objectif est de mutualiser la partie dite « logique » de notre code JavaScript, par exemple : notre store Redux, nos hooks personnalisés, nos fonctions utilitaires, notre classe Api etc.
Notre application Symfony + React
Assurez-vous de remplir les prérequis du Framework Symfony. Ceci étant fait, commençons ensemble par créer un nouveau projet dans lequel nous installons notre application, celle-ci répondra au nom de website :
mkdir react-workspaces
cd react-workspaces && symfony new website --full
composer require symfony/webpack-encore-bundle
yarn install
yarn add @babel/preset-react --dev
yarn add react react-dom prop-types
// website/webpack.config.js
Encore
// ...
.enableReactPreset()
symfony server:start
La commande ci-dessus exécute notre server web localement, nous devrions obtenir le résultat suivant :
Jusqu'ici tout va bien, continuons 💪
Notre application React Native
Créons notre application mobile React Native à la racine de notre projet, sous le nom mobile :
npx react-native init mobile
cd mobile && yarn ios/android
Notre simulateur, iOS ou Android selon notre choix, doit apparaître tel quel :
Excellent, nous avons maintenant deux projets fonctionnels ✅
Yarn workspaces, la mutualisation facile
Yarn workspace nous permet de lier nos projets entre eux, et de mutualiser leurs différentes dépendances Node par la même occasion. Pour ce faire, nous déclarons trois workspaces : website, mobile et core, ce dernier est destiné à accueillir notre code JavaScript partagé.
// package.json
{
"name": "react-workspace",
"version": "0.0.1",
"private": true,
"workspaces": {
"packages": ["core", "mobile", "website"]
},
"devDependencies": {}
}
// core/package.json
{
"name": "core",
"version": "0.0.1"
}
// mobile/package.json
{
"name": "mobile",
"version": "0.0.1",
"dependencies": {
// ...
"core": "0.0.1"
}
}
// website/package.json
{
"name": "website",
"version": "0.0.1",
"dependencies": {
// ...
"core": "0.0.1"
}
}
yarn install
Le liant est appliqué ! Nous observons un nouveau dossier node_modules
à la racine de notre projet et notre workspace core est accessible depuis nos applications, i.e :
import myFunction from 'core/myFunction'
Lorsque vous versionnez votre projet avec Git, pensez à ajouter les
node_modules
dans un
.gitignore
placé à la racine du projet.
Néanmoins il nous reste quelques détails à régler avant de pouvoir exploiter nos workspaces. À ce stade si nous tentons de compiler notre application React Native, celle-ci échouera faute de trouver ses dépendances. Mince, pourtant nous avons bien mis en place nos workspaces, et nos packages sont bien présents dans notre dossier ./node_modules
. Que se passe-t-il ?
React Native n’est pas compatible avec notre approche basée sur les workspaces. La librairie ne peut pas être placée en dehors de la racine de notre projet mobile. Heureusement il existe une solution pour remédier à ce problème : nohoist
// package.json
{
"name": "react-workspace",
"version": "0.0.1",
"private": true,
"workspaces": {
"packages": ["core", "mobile", "website"],
"nohoist": ["**/react-native", "**/react-native/**"]
},
"scripts": {
"reset-modules": "rm -rf node_modules/ ./*/node_modules",
"reset-yarn": "yarn cache clean",
"reset-rn": "watchman watch-del-all; rm -fr $TMPDIR/react-*; rm -rf $TMPDIR/haste-map-react-native-packager-*",
"reset-cache": "yarn reset-yarn && yarn reset-rn",
"reset": "yarn reset-modules && yarn reset-cache"
},
"devDependencies": {}
}
yarn reset && yarn install
Nos packages liés à React Native sont désormais dans le dossier ./mobile/node_modules
. Nous avons également ajouter une tâche yarn reset destinée à nettoyer notre projet.
Notre projet mobile est de nouveau opérationnel ✅
Mutualiser notre code JavaScript
Dans cet exemple nous allons mettre en place un store Redux avec l’aide du framework Rematch. Si vous n'êtes pas familier avec ce dernier, nous vous invitons à lire l’article de Baptiste : All Star #1 : Rematch
Mise en place de notre store avec Redux + Rematch
Nous allons utiliser notre store redux dans nos deux applications, pour ce faire nous allons le mettre en place dans notre core workspace.
cd core
yarn add @rematch/core
// core/models.js
export const count = {
state: 0,
reducers: {
increment(state, payload) {
return state + payload
},
},
effects: (dispatch) => ({
async incrementAsync(payload, rootState) {
await new Promise((resolve) => setTimeout(resolve, 1000))
dispatch.count.increment(payload)
},
}),
}
// core/store.js
import { init } from '@rematch/core'
import * as models from './models'
const store = init({
models,
})
// core/selector.js
export const getCount = (state) => state.count
Notre store est prêt à l'emploi ✅
Utiliser notre store dans notre app Symfony + React
Avant d’entamer l’écriture de notre code JavaScript, nous devons créer un Controller représentant la homepage de notre application Symfony :
cd website
php bin/console make:controller HomeController
// website/src/Controller/HomeController.php
// ...
@Route("/", name="home")
<!-- website/templates/base.html.twig -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}
</body>
</html>
<!-- website/templates/home/index.html.twig -->
<!-- ... -->
{% block body %}
<div id=count-example”></div>
{% endblock %}
Nous consommons notre store redux dans notre app React via react-redux :
yarn add react-redux
// website/assets/js/app.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider, useDispatch, useSelector } from 'react-redux'
import store from 'core/store'
import { getCount } from 'core/selectors'
const Home = () => {
const dispatch = useDispatch()
const count = useSelector(getCount)
const increment = () => dispatch.count.increment(1)
const incrementAsync = () => dispatch.count.incrementAsync(1)
return (
<div>
The count is {count}
<button onClick={increment}>increment</button>
<button onClick={incrementAsync}>incrementAsync</button>
</div>
)
}
ReactDOM.render(
<Provider store={store}>
<Home />
</Provider>,
document.getElementById('count-example')
)
Sans oublier de permettre à webpack de compiler le code de notre core workspace présent dans les node_modules
:
// website/webpack.config.json
Encore
// ...
.configureBabel(
(babelConfig) => {
babelConfig.plugins.push('@babel/transform-runtime')
},
{
includeNodeModules: ['core'],
}
)
A ce stade nous devrions obtenir une interface opérationnelle à cette adresse https://127.0.0.1:8000/ 👇
Utiliser notre store dans notre app mobile React Native
Nous consommons également notre store redux via react-redux. Cependant, la librairie nécessite d'être dans le même répertoire que celui de React Native pour fonctionner. Nous allons donc de nouveau faire appel à nohoist pour que celui-ci reste dans les node_modules
du projet ciblé.
// package.json
{
// ...
"workspaces": {
"nohoist": ["**/react-native", "**/react-native/**", "mobile/react-redux"]
}
}
yarn add react-redux
// mobile/App.js
import React from 'react'
import { Provider } from 'react-redux'
import store from 'core/store'
import Home from './src/Home'
const App = () => {
return (
<Provider store={store}>
<Home />
</Provider>
)
}
export default App
// mobile/src/Home.js
import React from 'react'
import { Button, Text, View } from 'react-native'
import { useDispatch, useSelector } from 'react-redux'
import { getCount } from 'core/selectors'
const Home = () => {
const dispatch = useDispatch()
const count = useSelector(getCount)
const increment = () => dispatch.count.increment(1)
const incrementAsync = () => dispatch.count.incrementAsync(1)
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<View style={{ flex: 0, justifyContent: 'center' }}>
<Text>The count is {count}</Text>
<Button onPress={increment} title="increment" />
<Button onPress={incrementAsync} title="incrementAsync" />
</View>
</View>
)
}
export default Home
Nous devrions obtenir une interface similaire à celle de notre app Symfony 👇
Nous arrivons au bout de notre exemple, si vous le souhaitez vous pouvez retrouver le code source sur le repository suivant : github.com/premieroctet/react-workspaces. Nous y avons également ajouté une dose de TypeScript.
Et ensuite ?
Nous avons vu comment faire communiquer nos différents workspaces, vous pouvez pousser la complexité plus loin en augmentant leur nombre. Vous pouvez également partager vos composants avec React Native for web ou encore mutualiser vos hooks React par exemple.
Dites-nous si vous avez/allez tenter l'expérience.
👋