Meteor.js avec React et Typescript

Meteor.js avec React et Typescript

Alexandre P. dans Dev - Le 03-07-2022

Meteor est une plateforme js fullstack qui fait sa vie depuis bientôt 10 ans avec une communauté plutôt active. Son principal avantage est son approche reactive, Meteor.js se veut être une plateforme temps réel. Il accepte la syntaxe Typescript et permet de coder les vues en React depuis quelques années déjà. Que demander de mieux ?

Organiser le code avec Meteor

Meteor.js s'appuie sur Node.js et intègre nativement plusieurs fonctionnalités et librairies, tels que MongoDB, les sockets, etc...

Il s'agit d'une plateforme fullstack qui permet de coder à la fois le front et le back dans des fichiers partagés qui peuvent être importé de n'importe où. C'est pourquoi il faut souvent vérifier dans le code où est-ce que l'on se positionne avant d'exécuter un code uniquement backend ou uniquement frontend.

meteor-js ><

Pour savoir si le code est exécuté coté client on fait :

import { Meteor } from 'meteor/meteor'

if (Meteor.isClient) {
  // ... code exécuté uniquement sur le client
}

Pour le serveur, ce serait :

import { Meteor } from 'meteor/meteor'

if (Meteor.isServer) {
  // ... code exécuté uniquement sur le serveur
}

Comment s'effectue sa communication backend/frontend ?

Meteor fait communiquer ces deux briques via ce qu'il appelle le protocole DDP (pour Distributed Data Protocol) qui utilise à la fois :

  • du RPC (Remote Procedure Call)
  • le fait de pouvoir souscrire à des événements
  • faire des stream etc...

Comprenez bien que ce protocole cadre énormément les choses par la suite !

Cela signifie que globalement vous ne ferez pas de REST API (même si vous pouvez en faire via des extensions), de même vous ne ferez pas de GraphQL (même si vous pouvez en faire via des extensions 😅).

Ainsi cela implique moins d'intervention sur la partie routing du projet, l'organisation se fera ailleurs via :

  • La publication de données : Meteor.js permet d'exposer une partie des données DB côté front (en lecture uniquement), on peut faire des requêtes directement depuis la partie cliente, mais à condition d'avoir autorisé le dataset et avec des pré-conditions de filtres (par exemple une clé ownerId ou companyId serait forcée côté client pour empêcher de requêter des données dont on est pas propriétaire). Côté serveur toutes les données restent accessibles... Ingénieux ! 🧐
  • La création de méthodes : Le server défini un certains nombres de fonctions server-side que l'on peut appeler avec certains paramètres pour qu'il exécute du code non exposé au front. Généralement on y met les opérations d'écritures vers la base de données, écriture de fichiers, appels API, etc...
  • L'appels de ces méthodes : Le client peut appeler les server-side methods et exécuter des insertions en base de données, des updates, etc. via l'utilisation de Meteor.call.

L'influence du DDP de Meteor sur l'organisation !

Autre plateforme, autre méthodo, je vous propose mon approche lorsque je travaille sur un projet Meteor. Cela demande de l'organisation afin de ne pas perdre ses repaires car nous sortons des sentiers battus. Dans cet article, je ne mentionnerai pas la définition des types qui doit être faite en amont (la base de Typescript !).

Préparation du code Meteor.js côté server

On défini notre collection Mongo :

// /imports/api/products.ts

import { Meteor } from 'meteor/meteor'
import { Mongo } from 'meteor/mongo'

export const ProductCollection = new Mongo.Collection<Product>('products')

J'utilise le fichier index pour centraliser tous les modèles et permettre de faire un import unique sur mon module serveur plus tard.

// /imports/api/index.ts
// On exporte nos modèles ici

// ....
export { ProductCollection } from './products'

Il est temps d'exposer nos données sur le frontend, mais ceci ne se fait pas sans une sécurité préalable ! Ici nous exposerons uniquement les produits appartenant à une même compagnie sur le frontend. Ce filtre sera appliqué par défaut pour toutes les requêtes DB qui arrivent du client. De même si l'on ajoute des conditions supplémentaires, elles viendront s'ajouter à celles existantes sur ce qui a été publié.

Par exemple : companyId = 'ABCD' + name = 'clé usb'

Idem pour la pagination, les skip etc...

// /imports/api/published/product-publ.ts

import { Meteor } from 'meteor/meteor'
import { ProductCollection } from '../product'

Meteor.publish('products', function (companyId: string) { 
  if (!companyId) return null
  return ProductCollection.find({
    companyId
  })
})

Le fichier index du dossier published permet de centraliser tous les sous-modules :

// /imports/api/published/index.ts

import './product-publ'

Enfin nous importons tout cela dans notre Meteor server :

// /server/main.ts

import '/imports/api/published'
import '/imports/api'

Préparation du code Meteor.js côté client

Lorsque j'utilise Meteor, je gère les pages avec un react-router traditionnel. Mais j'organise toujours mes pages de manière à exporter le composant avec un Higher Order Component (appelé aussi HoC) qui permet de lier la partie vue à la couche de données et de souscrire aux événements (à la manière d'un redux connect).

Pratique pour le côté temps réel 👌!

// /imports/pages/products.tsx

import * as React from 'react'
import { useFind, useSubscribe, withTracker } from 'meteor/react-meteor-data'
import { ProductCollection } from '/imports/api' // On expose l'objet Mongo sur le front

type ProductsPageProps = {
  products: Products[]
}

function ProductsPage({ products }: ProductsPageProps) {
  return (
    <table>
      <thead>
        <tr>
          <td>Name</td>
          <td>Price</td>
        </tr>
      </thead>
      <tbody>
        {
          products.map((product: Product, index: number) => {
            return <tr key={index}>
              <td>{product.name}</td>
              <td>{product.price} €</td>
            </tr>
          })
        }
      </tbody>
    </table>
  )
}

// Mapping avec les données
export default withTracker(() => {
  // Hook de récupération de ma société (cookie ou session ou localStorage...)
  const { activeCompany } = useCompany()
  // On souscrit aux produits pour pouvoir les requêter
  const isLoading = useSubscribe('products', activeCompany) 
  
  
  const products = useFind(
    () => ProductCollection.find({}),
    [ ]
  )

  return {
    products 
  }
})(ProductsPage)

Les sauvegardes en base de données avec Meteor

Toujours dans notre fichier d'api, pour la partie serveur, nous allons ajouter une méthode Meteor afin d'insérer la donnée en base :

// /imports/api/products.tsx

import { Meteor } from 'meteor/meteor'
import { Mongo } from 'meteor/mongo'

export const ProductCollection = new Mongo.Collection<Product>('products')

Meteor.methods({
  'product.create'(companyId: string, data: Product) {
    try {
      return ProductCollection.insert({
        ...data,
        companyId
      })
    } catch (err) {
      console.log(err)
    }
  }
})

Côté vue, on se crée une page de formulaire. Personnellement j'utilise react-hook-form, mais je reviendrai un peu plus en détail sur cette librairie dans un prochain article.

// /imports/pages/product-add.tsx

import * as React from 'react'
import { useSubscribe, withTracker } from 'meteor/react-meteor-data'
import { useNavigate, useParams } from 'react-router-dom'

type ProductAddPageProps = {
  onSubmit: (data: Partial<Product>) => void
}

function ProductAddPage({ onSubmit }: ProductAddPageProps) {
  // ... formulaire sur lequel j'attache mon submit
})

export default withTracker(() => {
  const navigate = useNavigate()
  const { activeCompany } = useCompany()
  
  return {
    onSubmit: (data: Product) => {
      Meteor.call('product.create', activeCompany, data, (err: any, productId: string) => {
        alert('Product inserted')
        navigate('/products')
      })
    }
  }
})(ProductAddPage)

A ce stade vous savez faire de la lecture/écriture en base avec Meteor.js React et Typescript.

Dans de prochains articles nous verrons comment gérer les uploads et la pagination de données. Stay tuned ! 😃

#meteor#react#typescript#programming

user picture
Alexandre P.

Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.