Z zaratan@next

Migrer depuis Erb vers React

Le but de cet article est de donner une marche à suivre pour migrer à sa propre vitesse d’un front rails classique (ici à base de Erb) vers une solution basée sur un front en JS (ici en React) géré par Webpacker.

DISCLAMER : Ceci n’est pas un tuto React. Pour un tuto basique pour React, vous pouvez aller ici.

Présentation de l’app

On va prendre ici une application assez basique comme exemple. C’est réplicable sur des applications de taille normale, mais c’est plus long.

Il s’agit d’une seule page pour visualiser des posts et 3 actions, Create/Update/Delete. La page est protégée par une authentification via devise et affiche au besoin des erreurs de créations/update. De plus, l’application vérifie que vous avez bien les droits d’effectuer une action avant de la faire via la gem pundit.

Vous pourrez trouver le code ici.

Ceci devrait ressembler peu ou prou à la stack d’une application standard de Rails.

Présentation de Webpacker

Webpacker est un moyen d’utiliser facilement Webpack en conjonction avec Rails.

Qu’est ce que Webpack ? C’est un outil pour compacter et compiler des assets ensemble, à l’image de l’assets pipeline de Rails. Il est très utilisé dans l’écosystème JavaScript.

(Si vous vous demandez pourquoi j’ai écrit “facilement”, je vous renvoie vers mon article sur le sujet.)

Pourquoi feriez-vous ça ?

Plusieurs raisons peuvent vous pousser à ajouter Webpacker dans votre application.

  • Vous voulez utiliser facilement les nombreux packages disponibles de l’écosystème JavaScript sans les brancher dans l’assets pipeline.
  • Vous voulez amener React/Vue/Else dans votre application et ne voulez pas recréer une application front ex-nilo, mais déplacer petit à petit des bouts de votre app vers une de ces applications, sans recoder toutes les vues de Devise par exemple.

Le présent article s’adresse plus au point 2 qu’au point 1, même si vous pourriez utiliser pas mal de points de cet article pour vous aider dans le premier cas.

Initialisation de Webpacker

Si vous voulez suivre la vidéo en même temps : Youtube (jusqu’à 26:52)

On va commencer par faire le bout de plomberie qui va nous permettre de travailler ensuite. C’est à dire amener une app React minimaliste à l’intérieur d’une de nos vues.

Ces étapes là vont aller assez vite.

Installation de Webpacker

On va directement suivre la doc de webpacker pour ça (et on va prendre soin d’utiliser la version @next de webpacker qui apporte un bon lot de fonctionnalités).

On ajoute a notre Gemfile :

1
gem 'webpacker', '>= 4.0.x'

Puis on installe les différents packages

1
2
bundle install
yarn add @rails/webpacker@next

Enfin on initialise Webpacker

1
bundle exec rails webpacker:install

Ici vous avez déjà webpacker presque opérationnel. On va effectuer quelques petites modifications dans notre config/webpacker.yml :

  • Remplacer tous les localhost par 0.0.0.0. Ça fait que vous pouvez accéder à votre site sur n’importe quelle URL et vous aurez accès à Webpack.
  • Changer extract_css à true, ça fera que tous vos fichiers CSS importés seront automatiquement exportés.
  • En development, changez hmr à true. Vous ne déclencherez plus de full reload à chaque changement de component dans React.

Installation de React

On laisse webpacker ajouter les fichiers minimaux pour notre app React :

bundle exec rails webpacker:install:react

On va ensuite renommer ça et déplacer des fichiers. C’est l’occasion de parler un peu du dossier app/javascript. Vous aurez dedans un dossier spécial nommé packs. Dans ce dossier ne devra aller que les endpoints que vous voulez exposer dans votre application (ici on n’en veut qu’un seul : application.js).

  • Supprimez le fichier app/javascript/packs/application.js.
  • Renommez le fichier app/javascript/packs/hello-react.jsx en app/javascript/packs/application.jsx.
  • Créez un dossier app/javascript/src. C’est ici que nous mettrons tous les autres fichiers de l’application.
  • Créez un fichier app/javascript/src/App.jsx. Dans ce fichier créez un component React minimaliste.
  • Éditez votre fichier app/javascript/packs/application.jsx pour importer le component App et le render. Ensuite, supprimez le component par défaut contenu dans le fichier.

Normalement si vous avez bien tout fait, vos fichiers devraient ressembler à :

app/javascript/packs/application.jsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file,
// like app/views/layouts/application.html.erb. All it does is render <div>Hello React</div>div> at the bottom
// of the page.

import React from 'react';
import ReactDOM from 'react-dom';
import App from '../src/App';

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(<App />, document.getElementById('app-container'));
});
app/javascript/src/App.jsx
1
2
3
4
5
6
7
import React, { Component } from 'react';

export default class App extends Component {
  render() {
    return <div>Hello from React :)</div>;
  }
}

Votre “App” est prête à être plug dans vos views classiques.

Plug de votre application dans votre/vos views

Dans votre view (ici /app/view/posts/index.js), on va ajouter en fin de fichier la ligne :

1
<%= javascript_pack_tag 'application' %>

De plus, on va ajouter la div qui recevra notre app à terme :

1
<div id="app-container"></div>

Il ne reste plus qu’à tester maintenant.

Lancez votre app webpack-dev-server

Il faut lancer les deux serveurs dans 2 terminaux différents :

1
2
3
4
5
# Votre serveur Rails classique
rails s

# Le serveur de dev webpack qui va se charger de recompiler vos JS a chaque changement.
bin/webpack-dev-server

Votre première vue

Méthodologie

Je vais vous donner quelques conseils (et avis) quant à l’architecture de votre App et comment procéder à la migration :

  • Vos “Données” (ici les posts et le current_user) devraient vivre dans votre component App et être donné aux enfants. Un seul point de vérité.
  • Vos méthodes pour récupérer vos données et faire des actions devraient aller dans app/javascript/src/APIs (ici dans un fichier posts.js).
  • Vos components devraient aller dans un dossier app/javascript/src/components.
  • Déplacez votre erb tel quel et éditez-le dans le component. Ça vous évitera d’oublier des choses.
  • Commencez par afficher les choses puis ajoutez ensuite les actions une par une avec votre backend.

Récupérer des données

Si vous voulez suivre la vidéo en même temps : Youtube (jusqu’à 53:13)

Pour récupérer nos données et les afficher on va procéder en 4 étapes.

1. Faire en sorte que rails réponde du JSON.

On utilise respond_to et format (doc), et on renvoie du JSON (pour des raisons de facilité je conseille d’utiliser ActiveModel Serializer)

Pour une utilisation facile mais pratique de ActiveModel Serializer il faut configurer son adapteur en mode :json. config/initializers/ams.rb

1
ActiveModelSerializers.config.adapter = :json

On crée nos serializers de cette façon :

app/serializers/post_serializer.rb
1
2
3
4
5
class PostSerializer < ActiveModel::Serializer
  attributes :id, :text

  belongs_to :author, serializer: AuthorSerializer
end
app/serializers/author_serializer.rb
1
2
3
class AuthorSerializer < ActiveModel::Serializer
  attributes :id, :username
end

Ces fichiers permettent de définir les informations qui seront renvoyées pour chaque type d’objet.

Puis on édite notre controller pour transformer notre méthode index depuis : app/controller/posts_controller.rb

1
2
3
4
def index
  @new_post = Post.new
  @posts = Post.all.order(created_at: :desc).includes(:author)
end

Vers :

app/controller/posts_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def index
  @posts = Post.all.order(created_at: :desc).includes(:author)
  respond_to do |format|
    format.html do
      @new_post = Post.new
    end
    format.json do
      render json: @posts
    end
  end
end

Le but ici étant de créer un comportement différent quand on nous demandera du JSON.

Le format de réponse de notre API sera :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "posts": [
    {
      "id": "…",
      "text": "text post",
      "author": { "id": "…", "username": "zaratan" }
    },
    …
  ]
}

Il va être temps d’appeler cette API depuis votre app React \o/.

2. Créer un fichier pour faire des appels API et lire du JSON.

On crée un fichier simple en JS pour faire cet appel en utilisant fetch:

app/javascript/src/APIs/posts.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
export const fetchPosts = async () => {
  // On va chercher la donnée
  const postsResponse = await fetch('/posts', {
    // Ces headers nous permettent de dire a notre app Rails: Je veux du json PLZ
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
  });
  // On parse le body de la réponse en json pour obtenir un objet JS.
  const postsJSON = await postsResponse.json();
  // On supprime le fait que les posts soient rangés dans "posts"
  return postsJSON.posts;
};

3. Appeler ces méthodes dans le component App.

Notre component App va être responsable des données.

On lui déclare donc un state :

1
2
3
4
state = {
  posts: []
}

Puis, une fois que le component est chargé sur la page, on lui dit d’aller chercher les données :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { fetchPosts } from './APIs/posts';

[…]

export default class App extends Component {
  state = {
    posts: []
  }

  refreshPosts = async () => {
    const posts = await fetchPosts();
    this.setState({
      posts,
    });
  };

  componentDidMount = async () => {
    await this.refreshPosts();
  };

  […]
}

4. Se servir de ces données pour afficher des components.

On se sert ensuite des données qui seront dans notre state à un moment pour afficher les différents Post.

Envoyer des données et actions

Si vous voulez suivre la vidéo en même temps : Youtube (jusqu’à 1:12:56)

Vous procédez de même pour les différentes actions en ajoutant les méthodes correspondantes dans le fichier d’API et dans le controller.

Example du create :

app/controller/posts_controller.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def create
  post = Post.create!(create_params.merge(author: current_user))
  respond_to do |format|
    format.html do
      redirect_to root_path
    end
    format.json do
      render json: post
    end
  end
end
app/javascript/src/APIs/posts.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const addPost = async ({ text }) => {
  const postResponse = await fetch('/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    },
    body: JSON.stringify(addCsrf({ post: { text } })),
  });
  const postJSON = await postResponse.json();
  return postJSON;
};

Les points importants sont :

Le CSRF

Dans toutes les méthodes qui ne sont pas des GET, vous allez avoir besoin de renseigner le CSRF (généré automatiquement par Rails).

Personnellement, je le gère de cette façon :

J’ai une méthode addCsrf :

1
2
3
4
5
6
7
const addCsrf = object => {
  const token = document.querySelector('meta[name=csrf-token]').content;
  const key = document.querySelector('meta[name=csrf-param]').content;
  object[key] = token;
  return object;
};

que j’utilise pour compléter les body de mes requêtes :

1
2
body: JSON.stringify(addCsrf({ post: { text } })),

La gestion des erreurs

Personnellement je renvoie des trames JSON ressemblant à :

1
{ "errors": ["str_1", …]}

Et après ?

Le Routing

J’utilise le routing de Rails tant que je ne peux pas migrer complètement à React. Chaque page à sa propre App (que je renomme page, par exemple PostsPages, UserPage, etc…)

Le CSS

Vous pouvez ensuite migrer votre css dans React en utilisant une des nombreuses (soupir) solutions disponibles (Ma préférée étant StyledComponents en ce moment mais j’en ferai peut-être un article à part entière).

Le déploiement

Sur Heroku vous n’avez rien à faire de plus :) C’est pas magique ?

La pagination

ActiveModelSerializers gère très bien la pagination avec Kaminari :)

Conclusion

Voici le code final de l’exercice : https://github.com/denispasin/rails_to_webpack/pull/1

Ça devrait faire un bon article pour débuter une migration d’un Front Rails vers React. Je ne conseille en aucun cas de tout migrer d’un coup. Le travail peut être titanesque et vous avez clairement mieux à faire de votre temps.

À bientôt <3