AccueilClients

Applications et sites

  • Application métiersIntranet, back-office...
  • Applications mobilesAndroid & iOS
  • Sites InternetSites marketings et vitrines
  • Expertises techniques

  • React
  • Expo / React Native
  • Next.js
  • Node.js
  • Directus
  • TypeScript
  • Open SourceBlogContactEstimer

    19 mars 2020

    Partagez votre code JavaScript entre vos applications React Native, React et Symfony

    6 minutes de lecture

    Partagez votre code JavaScript entre vos applications React Native, React et Symfony

    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 :

    Symfony

    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 :

    Application React Native

    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/ 👇

    Symfony+React+Redux

    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 👇

    React Native + React Redux

    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.

    👋