julien-fournier-initial
Julien Fournier
Awesome Developer

Démarrer avec Next, le micro framework basé sur React

Frontend, React21/12/2016

Créer un site web disposant d'un rendu serveur multi-pages, grâce au micro framework "Next" basé sur ReactJS.

Créé et publié très récemment en open source par Zeit, Next est un micro framework pour créer des applications web. En un minimum de temps et de configuration, on peut créer un site disposant d'un rendu serveur (server-side rendering aka. SSR), multi-pages, à l'écoute de ses utilisateurs et qui réagit en conséquence. Pour cela, il se repose entre autres sur NodeJS, React, Babel et Webpack.

Ses principaux avantages sont :

  • Aucune configuration (ou presque) : pas de temps perdu à configurer Babel ou Webpack ;
  • Application universelle : les pages sont rendues de la même manière sur le serveur et sur le client ;
  • Live reload, inutile en production mais assez agrèable pendant les phases de dév.

Dans cet article de découverte, on évoquera :

  • la mise en place du projet ;
  • la création d'une première page, simple, au design à couper le souffle... Un Hello world en somme ;
  • la découverte du routing selon Next ;
  • la récupération des données et leur rendu côté serveur ;
  • l'application d'un style de base ;
  • le routing dynamique.

Le résultat est visible à l'adresse suivante.

Initialisation

On commence par créer un dossier de travail :

$ mkdir next && cd next

On initialise npm :

$ npm init

Et on ajoute Next à la liste des dépendances :

$ npm install --save next

Enfin on ajoute la commande de lancement aux scripts du packages.json :

{
  "scripts": {
    
    "dev": "next"
  }
}

Ajouter une première page

On commence par créer un dossier "pages" à la racine du projet :

$ mkdir pages

Next se base sur l'arborescence des fichiers pour gérer ses routes. Ainsi l'arborescence du dossier, ./pages, définira l'arborescence de notre site.

Pour avoir une page d'accueil, il nous suffit donc de créer un fichier "index.js" dans le dossier ./pages qui exporte un composant React.

./pages/index.js

import React from 'react';

export default class Home extends React.Component {
  render() {
    return (
      <h1>Bonjour !</h1>
    );
  }
}

Vous pouvez maintenant lancer le serveur :

$ npm run dev

et ainsi apprécier le fruit de votre dur labeur à l'adresse suivante :

http://localhost:3000

Utilisation du routing

On va maintenant ajouter une page qui recevra les actualités concernant le monde du mobile glanées sur un célèbre journal étranger. Cette page sera accessible à l'adresse http://localhost:3000/mobile.

Comme évoqué précédemment, le dossier ./pages correspond à la base du site http://localhost:3000/. De ce fait, Next propose deux solutions pour permettre l'accès à la page http://localhost:3000/mobile.

  • Soit on crée un dossier ./pages/mobile contenant un fichier index.js :

    - pages
    |_ index.js
    |_ mobile
     |_ index.js
    
  • Soit on crée un fichier à la racine du dossier ./pages nommé mobile.js :

    - pages
    |_ index.js
    |_ mobile.js
    

Toutefois, je trouve personnellement qu'utiliser des dossiers permet de garder un projet plus facile à maintenir.

./pages/mobile/index.js

import React from 'react';

export default class Mobile extends React.Component {
  render() {
    return (
      <div>
        <h1>Mobile</h1>
      </div>
    );
  }
}

Une fois que vous avez sélectionné la façon de faire qui vous correspond le plus, vous pouvez accéder à votre page :

http://localhost:3000/mobile

Ajoutons alors simplement des liens pour naviguer entre nos 2 pages.

./pages/index.js

render() {
    return (
      <div>
        <h1>Home</h1>
        <a href="/mobile">
          Mobile
        </a>
      </div>
    );
  }

./pages/mobile/index.js

import React from 'react';

export default class Mobile extends React.Component {
  render() {
    return (
      <div>
        <h1>Mobile</h1>
        <a href="/">
          Retour
        </a>
      </div>
    );
  }
}

Toutefois, vos liens se comportent comme des liens HTML traditionnels. Pour avoir un comportement plus proche de ce qu'on peut attendre d'une application React (navigation instantané, pas de rafraichissement), il suffit d'utiliser l'élément <Link/> de Next.

Dans vos imports, ajoutez :

import Link from 'next/link';

Et simplement englobez vos liens dans cet élément en déplaçant l'attribut href sur le <Link/>.

...
<Link href="/mobile">
  <a>Mobile</a>
</Link>
...

En résumé :

./pages/index.js

import React from 'react';
import Link from 'next/link';

export default class Home extends React.Component {
  render() {
    return (
      <div>
        <h1>Home</h1>
        <Link href="/mobile">
          <a>Mobile</a>
        </Link>
      </div>
    );
  }
}

./pages/mobile/index.js

import React from 'react';
import Link from 'next/link';

export default class Mobile extends React.Component {
  render() {
    return (
      <div>
        <h1>Mobile</h1>
        <Link href="/">
          <a>Retour</a>
        </Link>
      </div>
    );
  }
}

A ce point, vous disposez de 2 pages, avec un rendu serveur et une navigation. Voyons maintenant comment récupérer les données en conservant ce rendu côté serveur.

Récupération des données et rendu serveur

Comme tous les projets React, les données peuvent être récupérées dans la fonction componentWillMount() du composant. On utilisera ici isomorphic-fetch afin de disposer de fetch() côté serveur :

$ npm install isomorphic-fetch --save

./pages/mobile/index.js

import React from 'react';
import Link from 'next/link';
import 'isomorphic-fetch';

export default class Mobile extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      loading: true,
      news: []
    };
  }

  componentWillMount() {
    fetch('http://content.guardianapis.com/search?q=mobile&api-key=test')
      .then( response => response.json() )
      .then( res => {
        const results = res.response.results;

        return this.setState({
          loading: false,
          news: results
        });
      })
      .catch( err => this.handleError(err));
  }

  handleError(error) {
    this.setState({
      loading: false
    });
    alert(error);
  }

  render() {
    return (
      <div>
        <h1>Mobile</h1>
        <Link href="/">
          <a>Retour</a>
        </Link>

        {this.state.loading ? (
          <p>Chargement ...</p>
        ) : 
          this.state.news.length == 0 ? (
            <p>Aucun article à afficher</p>
          ):(
            <ul>
              {this.state.news.map((news) => <li key={news.id}>{news.webTitle}</li>)}
            </ul>
          )
        }
      </div>
    );
  }
}

On récupère les articles depuis l'API dans componentWillMount() et on les affiche. Le souci est qu'on ne dispose pas du rendu serveur. Si vous affichez le code source de la page, les titres des articles sont absents.

Grace à Next, la chose est simple. On dispose d'une fonction getInitialProps() qui nous permet de récupérer les données nécessaires au montage du composant, de façon synchrone et côté serveur grâce à l'utilisation de async / await (Pour plus d'information vous pouvez suivre cette présentation). Le rendu n'interviendra donc qu'à la fin de l'exécution de cette fonction.

Il nous suffit maintenant de déplacer et modifier un peu notre processus de récupération des données pour effectuer la récupération côté serveur.

./pages/mobile/index.js

import React from 'react';
import Link from 'next/link';
import 'isomorphic-fetch';

export default class Mobile extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      loading: props.loading,
      news: props.results
    };
  }

  static async getInitialProps(){
    let results = [];
    const res = await fetch('http://content.guardianapis.com/search?q=mobile&api-key=test')
      .then( response => response.json() );

    if (res && res.response.status === 'ok') {
      results = res.response.results;
    }

    return {
      results,
      loading: false
    }
  }

  render() {
    return (
      <div>
        <h1>Mobile</h1>
        <Link href="/">
          <a>Retour</a>
        </Link>

        {this.state.loading ? (
          <p>Chargement ...</p>
        ) : 
          this.state.news.length == 0 ? (
            <p>Aucun article à afficher</p>
          ):(
            <ul>
              {this.state.news.map((news) => <li key={news.id}>{news.webTitle}</li>)}
            </ul>
          )
        }
      </div>
    );
  }
}

Templating et gestion du style

Notre site d'actualités est beau et fonctionnel mais on va quand même ajouter une mise en page par le biais d'un composant qui importera nos fichiers CSS et JS, cela donnera un peu de douceur à notre présentation. C'est important la douceur ! Ce fichier n'étant pas une page, on ne va pas le créer dans le dossier pages, ainsi on évitera qu'il ne soit directement affiché à l'utilisateur.

On crée donc un composant <Layout /> qui recevra nos pages comme enfants.

./components/Layout/index.js

import React from 'react';

export default class Layout extends React.Component {
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    );
  }
}

Ne disposant pas de fichier du genre index.html dans lequel modifier le contenu de la balise <head>, Next a prévu un composant nous permettant de gérer ça facilement et qui s'appelle ? Et bien <Head> tout simplement. On y ajoute donc une feuille de style, on déplace nos liens et on en profite pour placer quelques classes CSS sur nos éléments :

./components/Layout/index.js

import React from 'react';
import Head from 'next/head';
import Link from 'next/link';

export default class Layout extends React.Component {
  render() {
    return (
      <div>

        <Head>        
          <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css" />
        </Head>

        <nav className="light-blue lighten-1" role="navigation">
          <div className="nav-wrapper container">

            <Link  href="/">
              <a id="logo-container" className="brand-logo">News</a>
            </Link>

            <ul className="right">
              <li>
                <Link href="/mobile">
                  <a>Mobile</a>
                </Link>
              </li>
            </ul>

          </div>
        </nav>

        <div className="container">
          {this.props.children}
        </div>

      </div>
    );
  }
}

./pages/index.js

import React from 'react';

import Layout from '../components/Layout';

export default class Home extends React.Component {
  render() {
    return (
      <Layout>
        <h1>Home</h1>
      </Layout>
    );
  }
}

./pages/mobile/index.js

import React from 'react';
import Link from 'next/link';
import 'isomorphic-fetch';

import Layout from '../../components/Layout';

export default class Mobile extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      loading: props.loading,
      news: props.results
    };
  }

  static async getInitialProps(ctx){
    let results = [];
    const res = await fetch('http://content.guardianapis.com/search?q=mobile&api-key=test')
      .then( response => response.json() );

    if (res && res.response.status === 'ok') {
      results = res.response.results;
    }

    return {
      results,
      loading: false
    }
  }

  render() {
    return (
      <Layout>
        <h1>Mobile</h1>
        <Link href="/">
          <a className="waves-effect waves-light btn light-blue darken-3">Retour</a>
        </Link>

        <div className="row">
          <div className="col s6 offset-s3">
            {this.state.loading ? (
              <div className="progress">
                  <div className="indeterminate"></div>
              </div>
            ) : 
              this.state.news.length == 0 ? (
                <p>Aucun article à afficher</p>
              ):(
                <ul className="card collection">
                  {this.state.news.map((news) => (
                    <li key={news.id} className="collection-item">
                      {news.webTitle}
                    </li>
                  ))}
                </ul>
              )
            }
          </div>
        </div>
      </Layout>
    );
  }
}

Gestion des routes dynamiques

On a vu précédemment l'utilisation des routes "simples", passons donc maintenant à l'utilisation de paramètres. Là où Next est (pour l'instant) décevant c'est dans la gestion des routes dynamiques. En effet, il ne gère que les paramètres d'URL, il n'est donc pas possible d'utiliser de jolies URLs avec une réécriture sans implémenter un proxy dans cet esprit là. Toutefois, j'ai bien dit "pour l'instant" car la communauté autour de Next considère ce point comme essentiel et travaille dessus. Sachant que Arunoda Susiripala (ceux d'entre vous s'étant déjà intéressés à MeteorJS le connaissent sûrement) a décidé d'abandonner Meteor au profit d'autres solutions dont Next, nul doute que le projet évoluera très vite pour répondre aux problématiques les plus bloquantes.

Trêve de blah blah et de potins. On crée une page view.js qui recevra un identifiant d'article dans l'URL et en affichera le contenu.

./pages/mobile/view.js

import React from 'react';
import Link from 'next/link';
import 'isomorphic-fetch';

import Layout from '../../components/Layout';

export default class Mobile extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      loading: props.loading,
      news: props.news
    };
  }

  static async getInitialProps(ctx){
    let news = {};
    const id = ctx.query.id;
    const res = await fetch(`http://content.guardianapis.com/${id}?api-key=2bdc6595-6460-4032-8a08-db5e870874eb&show-fields=body`)
      .then( response => response.json() );

    if (res && res.response.status === 'ok') {
      news = res.response.content;
    }

    return {
      news,
      loading: false
    }
  }

  render() {
    if(!this.state.news)
      return (
        <Layout>
          <h1>Article introuvable</h1>
          <Link href="/mobile">
            <a className="waves-effect waves-light btn light-blue darken-3">Retour</a>
          </Link>
        </Layout>
      );

    const { news } = this.state;

    return (
      <Layout>
        <h1>{news.webTitle}</h1>
        <Link href="/mobile">
          <a className="waves-effect waves-light btn light-blue darken-3">Retour</a>
        </Link>

        <div className="row">
          <div className="col s6 offset-s3">
            <div dangerouslySetInnerHTML={{__html:news.fields.body}} />
          </div>
        </div>
      </Layout>
    );
  }
}

La fonction getInitialProps vue précédemment reçoit l'objet ctx. Dans ce ctx, on peut extraire le paramètre query qui lui contient les paramètres de l'URL. On peut donc récupérer l'identifiant (id) de notre article pour le passer à l'API.

Pour être complet, on ajoute les liens à la liste des articles :

import React from 'react';
import Link from 'next/link';
import 'isomorphic-fetch';

import Layout from '../../components/Layout';

export default class Mobile extends React.Component {

  constructor(props) {
    super(props);

    this.state = {
      loading: props.loading,
      news: props.results
    };
  }

  static async getInitialProps(ctx){
    let results = [];
    const res = await fetch('http://content.guardianapis.com/search?q=mobile&api-key=test')
      .then( response => response.json() );

    if (res && res.response.status === 'ok') {
      results = res.response.results;
    }

    return {
      results,
      loading: false
    }
  }

  render() {
    return (
      <Layout>
        <h1>Mobile</h1>
        <Link href="/">
          <a className="waves-effect waves-light btn light-blue darken-3">Retour</a>
        </Link>

        <div className="row">
          <div className="col s6 offset-s3">
            {this.state.loading ? (
              <div className="progress">
                  <div className="indeterminate"></div>
              </div>
            ) : 
              this.state.news.length == 0 ? (
                <p>Aucun article à afficher</p>
              ):(
                <ul className="card collection">
                  {this.state.news.map((news) => (
                    <li key={news.id} className="collection-item">
                      <Link href={`/mobile/view?id=${news.id}`}>
                        {news.webTitle}
                      </Link>
                    </li>
                  ))}
                </ul>
              )
            }
          </div>
        </div>
      </Layout>
    );
  }
}

Vous avez maintenant votre liste d'articles ainsi que leur visualisation. Si vous regardez le code source, vous pouvez constater que notre contenu est bien présent. Nous avons donc en 15 minutes :

  • configuré un projet React ;
  • créé 3 pages (accueil, liste, vue) ;
  • géré le routing ;
  • rendu ces pages côté serveur.

Le résultat est visible à l'adresse suivante.

On prend un peu de recul

Next n'est clairement pas la solution à toutes vos problématiques, surtout si vous souhaitez avoir un contrôle complet de chaque élément de votre projet. Au moment de l'écriture, beaucoup de choses se déroulent encore dans les coulisses et sur lesquels on n'a pas du tout la main. Mais à mon avis, si vous aimez vraiment bosser avec React et que vous avez besoin d'un site "vitrine" ou d'un front simple et rapide pour votre projet, avec en cadeau le rendu serveur, donnez sa chance à Next.