Javascript ( 10 / 21 articles - Voir la liste )

Marque-page Marque-pages React

Contrairement à Angular, React propose peu de solutions par défaut aux problèmes les plus courants. Il faut souvent ajouter des modules en dépendance, et il est parfois difficile de ne pas se perdre dans la jungle des modules pour React.

Voici une liste de modules et de pratiques utiles pour les besoins courants.

Socle

Mise en place

On peut initialiser facilement un projet React avec l’outil CRA (Create React App). Il crée l’arborescence de base, via une simple commande comme npx create-react-app my-app --template typescript.

Routage

Le module react-router fait ça très bien.

Appels API

Pour récupérer des données auprès d’une API ou via d’autres méthodes asynchrones, on peut utiliser SWR ou react-query.

État

Si on souhaite gérer un état globale de l’application au niveau local, on peut utiliser redux avec redux-toolkit ou alors recoil.

Formulaires

Le module react-hook-form fait ça très bien. Il supporte notamment Yup pour gérer la validation de champs.

Internationalisation

Le module react-i18next permet de gérer une appli multilingue.

Design

CSS

Pour restreindre des classes CSS à un composant en particulier, on peut utiliser le concept de modules CSS.

Material

Les principales bibliothèques graphiques sont disponibles pour React, et notamment Material UI. Elle fonctionne très bien, sauf avec le hook-form. C’est problématique et cela nécessite de recréer un composant « Contrôlable », pour chaque type de champ de formulaire.

Qualité

Tests

Plusieurs bibliothèques pour les tests fonctionnent bien avec React, notamment jest.

Lint

On peut combiner prettier et eslint, pour normaliser la base de code. Il existe des extensions à eslint spécialement pour React, afin de détecter et remonter si certaines mauvaises pratiques sont utilisées.

Documentation des composants

Pour documenter et présenter une bibliothèque de composants, on peut utiliser Storybook. Cela permet de manipuler et tester chaque composant, dans une interface dédiée.

Autres

Base de donnée locale

Pour utiliser plus facilement la base de données locale IndexedDB, on peut y accéder via dexie.

Authentification avec JWT

Pour extraire les données d’un jeton JWT, on peut utiliser jwt-decode.

Marque-page Modèle de composant

Voici un modèle de composant React minimal :

// MyComponent.tsx
import React, { ReactNode } from 'react';
import styles from "./MyComponent.module.scss";

interface Props {
  children: ReactNode;
  notRequiredProperty?: string;
}

export const MyComponent: React.FC = ({ children, notRequiredProperty }: Props) => {
  return <div className={styles.myComponentClass}>{children}< /div>;
};

Explications :

  • Le composant est utilisable dans un autre composant, via <MyComponent children={<p>EXAMPLE</p>} />
  • On importe le fichier MyComponent.module.scss, dans lequel se trouve une règle CSS pour la classe myComponentClass.
  • Le composant est un wrapper, auquel on passe un sous-composant via la propriété children.
  • On déclare l’interface Props pour définir les propriétés du composant. Cela facilite l’autocomplétion et permet la validation du typage à la compilation.
  • La propriété notRequiredProperty est facultative lorsqu’on appelle le composant.

Héritage

Si on souhaite créer un wrapper pour un composant existant, on veut souvent qu’il accepte les mêmes propriétés. Dans ce cas, on peut déclarer l’interface et le composant ainsi :

type Props = SomeComponentToWrapProps & {
  extraProperty1: string
};

export const MyComponent: React.FC = ({ extraProperty1, inheritedProp1, ...others }: Props) => {
  // [...]
};

Explications :

  • Les propriétés du composant sont l’union de celles de SomeComponentToWrapProps, avec celle déclarée ici.
  • On indique les quelques propriétés dont on a explicitement besoin, et on peut accéder à toutes les autres via l’objet others.

Marque-page Construire une queryString

Voici une fonction pour construire une queryString à partir d’un objet.

Version JavaScript :

export const buildQueryString = (parameters) => {
  const paramParts = Object.entries(parameters)
    .map(([key, value]) => `${key}=${encodeURI(value)}`);

  return paramParts.length > 0 ? `?${paramParts.join('&')}` : '';
};

Version TypeScript :

export const buildQueryString = (parameters: { [key: string]: string }) => {
  const paramParts = Object.entries(parameters)
    .map(([key, value]) => `${key}=${encodeURI(value)}`);

  return paramParts.length > 0 ? `?${paramParts.join('&')}` : '';
};

Explications :

  • Chaque propriété de l’objet devient une clé de la queryString, dont la valeur échappée.
  • Les paramètres sont séparés par & et ? est ajouté en préfixe de la chaîne.

Astuce Debounce en React

Régulièrement en Javascript, on veut écouter les changements sur un élément, mais ne les prendre en compte qu’à intervalle régulier.
Par exemple, si on écoute le déplacement de la souris pour lancer un traitement, on n’a pas besoin de l’exécuter à chaque changement de position, mais uniquement tous les 300 ou 500ms.

Pour éviter l’effet « rebond », on utilise alors une fonction de debounce, ou dans React, un hook.

Script

import { useEffect, useState } from 'react';

/**
 * Delays the value change, so it’s only committed once during the specified duration.
 *
 * Usage :
 * <pre>
 * const debouncedValue = useDebouncedValue(realTimeValue, 500);
 * <pre>
 */
export const useDebouncedValue = <T>(input: T, duration = 500): T => {
  const [debouncedValue, setDebouncedValue] = useState(input);

  useEffect(() => {
    const timeout = setTimeout(() => {
      setDebouncedValue(input);
    }, duration);

    return () => {
      clearTimeout(timeout);
    };
  }, [input, duration]);

  return debouncedValue;
};

Exemple d’utilisation

On veut afficher un message dans la console au changement de valeur du champ de formulaire,
mais limiter ce traitement avec un debounce.

import React, { useEffect, useState } from 'react';
import { useDebouncedValue } from './utils/debounce';

export default (() => {
  const [myValue, setMyValue] = useState('');
  const myDebouncedValue = useDebouncedValue(myValue, 500);

  useEffect(() => {
    console.log('This log is displayed when the value of debouncedFilterNom changes', myDebouncedValue);
  }, [myDebouncedValue]);

  const handleValueChange = async (event: any) => {
    setMyValue(event.target.value);
  };

  return (
    <form>
      <input type="text" onChange={handleValueChange} value={myValue} />
    </form>
  );
}) as React.FC;

Astuce SessionStorage vs LocalStorage

Dans une application ou un site web, on peut stocker des informations dans le navigateur de l'utilisateur, pour un nom de domaine spécifique.

Deux approches principales : les cookies et le Storage. Pour la seconde, deux possibilités : sessionStorage ou localStorage.

cookie vs Storage

La principale différence, c'est que les cookies sont accessibles côté client et côté serveur. À l'inverse, il n'y a aucun moyen côté serveur d'accéder au contenu ni du localStorage ni du sessionStorage.

Remarque :

Quand on parle de "sessions" pour le sessionStorage, il n'y a aucun rapport avec les sessions entre client/serveur comme on peut les manipuler en PHP, par exemple.

session vs local

Les données stockées en sessionStorage sont rattachées à un onglet.
Si on recharge la page au sein d'un même onglet, les données sont conservées. En revanche, elles expirent à sa fermeture.

Les données stockées en sessionStorage sont rattachées à un onglet.

Remarque :

Selon le navigateur, si on restaure un onglet fermé précédemment, on récupère le sessionStorage tel qu'il était avant fermeture. C'est le comportement pour les navigateurs basés sur Chrome. De même, si on duplique un onglet ouvert, la copie hérite d'une copie du sessionStorage de l'onglet original.

Pour aller plus loin

Erreur Impossible de charger les dépendances depuis https://jcenter.bintray.com

Si vous construisez des APK Android avec Cordova, vous pouvez rencontrer ce genre d'erreur :

[...]
> Task :app:preReleaseBuild FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Could not resolve all files for configuration ':app:releaseRuntimeClasspath'.
> Could not resolve com.android.support:support-v4:27.+.
  Required by:
      project :app
   > Failed to list versions for com.android.support:support-v4.
      > Unable to load Maven meta-data from https://jcenter.bintray.com/com/android/support/support-v4/maven-metadata.xml.
         > Could not get resource 'https://jcenter.bintray.com/com/android/support/support-v4/maven-metadata.xml'.
            > Could not GET 'https://jcenter.bintray.com/com/android/support/support-v4/maven-metadata.xml'.
               > Read timed out
[...]

Cela est dû à la coupure du dépôt jcenter.

La solution pour le remplacer est décrite dans plein de tickets sur StakOverflow (ex: Question #67418153).
En bref : il faut modifier le fichier build.gradle.

Malheureusement, vous n'avez peut-être pas accès à ce fichier, si c'est une Plateforme d'Intégration Continue qui se charge du build.
Dans ce cas, ce fichier n'est probablement pas versionné, mais générée automatiquement dans un répertoire nommé platforms/.

Entre chaque build, veillez à bien supprimer ce répertoire platforms/. Ainsi, le fichier build.gradle sera régénéré en utilisant des dépôts corrects.

Astuce Liste des caractères unicode d'espaces spéciaux

Selon la langue, différents caractères d'espace peuvent être utilisés.

En français par exemple (même si les claviers ne nous en facilitent pas l'écriture), l'espace entre des guillemets et le mot qu'ils encadrent n'est normalement pas d'une taille normale. Même chose avant :.
Les éditeurs de traitement de texte (comme LibreOffice ou Word) connaissent mieux que nous cette norme d'écriture et font le remplacement automatiquement.

Quand on récupère une chaîne qui contient de tels espaces, cela peut poser des problèmes d'affichage (ex: génération d'un PDF).

Voici la liste de ces caractères spéciaux, pour PHP :

$spaceUnicodeChars = [
            '\x{0020}',
            '\x{00A0}',
            '\x{180E}',
            '\x{2000}',
            '\x{2001}',
            '\x{2002}',
            '\x{2003}',
            '\x{2004}',
            '\x{2005}',
            '\x{2006}',
            '\x{2007}',
            '\x{2008}',
            '\x{2009}',
            '\x{200A}',
            '\x{200B}',
            '\x{202F}',
            '\x{205F}',
            '\x{3000}',
            '\x{FEFF}',
        ];

Pour javascript :

const SPACE_UNICODE_CHARS = [
    '\u{0020}',
    '\u{00A0}',
    '\u{180E}',
    '\u{2000}',
    '\u{2001}',
    '\u{2002}',
    '\u{2003}',
    '\u{2004}',
    '\u{2005}',
    '\u{2006}',
    '\u{2007}',
    '\u{2008}',
    '\u{2009}',
    '\u{200A}',
    '\u{200B}',
    '\u{202F}',
    '\u{205F}',
    '\u{3000}',
    '\u{FEFF}',
  ];

Astuce Utiliser les promesses

Introduction

Lorsqu'on fait un appel à un webservice ou qu'on exécute une grosse requête en base de données, cela prend un certain temps. On sait ce qu'on est sensé récupérer mais pas quand, ce qui semble plutôt gênant pour lancer la suite du traitement (ex: afficher les résultats).

Ce qu'on appelle une promesse, c'est ce qu'on espère recevoir comme résultat (un tableau, un flux, un objet, ... ce qu'on veut). On code la suite du traitement à réaliser comme si on avait ce résultat, sauf qu'on part de l'objet Promise à la place.

Exemples

Si on n'utilise pas les promesses et qu'on utilise une fonction asynchrone, cela peut ressembler à ça :

function getNumberFromWebservice(successCllback, errorCallback) {
    try {
        // On récupère les données
        let number = xxx();
        successCllback(number);
    } catch {
        errorCallback('Impossible de récupérer le nombre.');
    }
}

getResultsFromWebservice(function (number) {
    console.log('Affichage du résultat :');
    console.log(number);
}, function (error) {
    console.error(error);
});

Cette méthode fonctionne bien, mais à quoi cela ressemblerait-il si on appelait une seconde fonction asynchrone au retour de la première ?

function getNumberFromWebservice1(successCllback, errorCallback) {
    try {
        // On récupère les données
        let number = xxx();
        successCllback(number);
    } catch {
        errorCallback('Impossible de récupérer le nombre.');
    }
}

function getNumberFromWebservice2(number1, successCllback, errorCallback) {
    try {
        // On récupère les données
        let number2 = yyy();
        successCllback(number1 + number2);
    } catch {
        errorCallback('Impossible de récupérer le nombre.');
    }
}

function displayResult(sum) {
    console.log('Affichage de la somme :');
    console.log(sum);
}

function displayError(error) {
    console.error(error);
}

// /!\ Début de callback hell /!\
getResultsFromWebservice1(function (number1) {
    getResultsFromWebservice2(number1, function (sum) {
        displayResult(sum);
    }, function (error) {
        displayError(error);
    });
}, function (error) {
    displayError(error);
});

Ca devient vraiment peu lisible dès qu'on multiplie les fonctions asynchrones à chaîner. On a des callback de callback de callback (cf. Callback hell).

Les promesses permettent une syntaxe plus lisible :

// Pas de callback hell, on évite l'écriture en imbrication
getResultsFromWebservice1()
    .then(function(number1) { 
        return getResultsFromWebservice2(number1);
    }),
    .then(function(sum) {
        console.log('Affichage de la somme :');
        console.log(sum);
    }),
    .catch(function (error) {
        console.error(error);
    });

// Ou même plus court avec la syntaxe allégée
getResultsFromWebservice1()
    .then((number1) => getResultsFromWebservice2(number1)),
    .then((sum) => displayResult(sum)),
    .catch((error) => displayError(error));

Le principe est le même sauf qu'on n'a plus besoin de passer les callback en paramètres à chaque fonction. Par contre pour que ça marche, il faut modifier un peu chaque fonction pour qu'elle ne retourne non plus un nombre mais un objet Promise :

function getNumberFromWebservice1() {
    return new Promise((resolve, reject) => {
        // On récupère les données
        let number = xxx();
        resolve(number);
    });
}

function getNumberFromWebservice2(number1) {
    return new Promise((resolve, reject) => {
        // On récupère les données
        let number2 = yyy();
        resolve(number1 + number2);
    });
}

Remarque :

Les try/catch ont été retirés dans les fonctions. Cela veut dire qu'en a cas d'erreur levée par l'appel aux webservices (xxx() et yyy()), ce sera le message de l'erreur qui sera affiché, et non plus le message 'Impossible de récupérer le nombre.'.

Aller plus loin

Les promesses existent depuis un moment en javascript. Il y a pas mal de cas d'utilisation :

  • exécuter trois tâches asynchrones une à une
  • exécuter trois tâches asynchrones une à une et en transmettant à chaque fois le résultat à la suivante
  • exécuter plusieurs tâches asynchrones en parallèle, puis une dernière quand elles sont toutes terminées
  • exécuter 2 séries de 3 tâches asynchrones en parallèle
  • ...

La plupart des framework proposent une couche supplémentaire pour gérer ces cas d'utilisation. Dans le monde de NodeJS par exemple, on utilise très souvent la lib async.

Documentation Mozilla Développeur :

Marque-page Introduction à NodeJS

Exercices

Pour faire ses premiers pas avec NodeJS, le site nodeschool.io propose des ateliers de développement.

Concrètement, il s'agit de petites applications NodeJS qui affichent un énoncé, et sont capables d'éxécuter votre programme pour vérifier qu'il correspond aux consignes. L'un des ateliers utiles pour débuter s'appelle learnyounode.

Accueil learnyounode

Pour l'utiliser il vous faut NodeJS :

curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs

Ensuite ça se passe avec npm (inclus vec NodeJS) :

npm install -g learnyounode
learnyounode

Express

Le framework Express semble très largement répandu dans l'univers de NodeJS, dès lors qu'on souhaite créer une application web (API REST, site front, ...).

Mozilla Developpeur propose un bon tutoriel pour démarrer avec Express.
Il s'agît d'un cas concret de création d'appplication web, pour gérer une bibliothèque (de livres).
Une partie front interroge un back REST full qui stocke les données dans une base MongoDB.

(Il y a également un atelier sur le sujet proposé sur nodeschool.io)

Mongoose

Pour utiliser une base MongoDB avec NodeJS, il existe l'API Mongoose.
(Elle est utilisée dans le tutoriel précédent.)

Astuce Bonnes pratiques jQuery

Isoler son code

Pour éviter que ses variables et fonctions ne soient disponibles partout et ne rentrent en conflit entre-elles, il est important d'isoler son code.

Pour cela, plusieurs solutions sont possibles, en fonction des cas.

Code à usage unique

Cela consiste à englober son code pour que les variables et fonctions deviennent locales.

(function($) {

    // Code à exécuter
    // [...]
})(jQuery);

Code à usage régulier

Cela consiste à placer ses variables et fonctions dans un namespace autre que celui par défaut (= lorsqu'on n'en précise pas).

$.myNamespace = {
    multiply: function(x,y) {
        return (x * y);
    }
};

// Appel
var calc = $.myNamespace.multiply(1, 1);

Code affectant des éléments du DOM 1

On utilise le namespace de jQuery : $ ou jQuery.

$.fn.myFunction = function(params) {

    this.css('color', 'red');
    return this;
};

// Appel
$('.element').myFonction();

Explication :

On crée un plugin jQuery, que l'on appelle sur un ensemble d'éléments du DOM (ex: $('.element')). Tous ces éléments sont impactés une fois, sans mémorisation de leur état (= stateless).

Code affectant des éléments du DOM 2

$.widget('ui.trsb.truc.myWidget', [$.ui.widgetParent,]{
    "options": {},
    myWidgetFunction(){...},
    _create: function(){...},
    _destroy: function(){...},
    _setOptions: function(key, value){...},
});

// Appel
$('.element').myWidget();
$('.element').myWidgetFunction();

Explications :

On crée un widget jQuery (nécessite jQueryUI), que l'on appelle sur un ensemble d'éléments du DOM (ex: $('.element')). Tous ces éléments sont impactés avec mémorisation de leur état (= statefull).

Les widgets sont utiles lorsque l'on veut proposer plusieurs méthodes d'action (ex: un accordéon avec une fonction pour dérouler, et une autre pour replier).

Normes de codage et optimisations

Sélecteurs

// Pour des raisons de compatibilité, préférez
$('#container').find('.children').hide();

// à 
$('#container .children').hide();

// ou à 
$('.children', '#container').hide();

Éviter le tout jQuery

$('#link').on('click', function() {

    // Préférez
    this.id

    // à
    $this.attr(id');
});

Chargement du DOM

// Préférez 
$(function() {

});

// à 
$(document).ready(function() {

});

Gestion des évènements

// Préférez 
$('#element').on('click', function() {

});

// à 
$('#element').click(function() {

});

No conflict

// Préférez 
(function($) {
 // [...] Code sans conflit
})(jQuery);

// à 
jQuery.noConflict();
// [...] Code sans conflit

Appels AJAX

// Préférez 
$.ajax()

// à 
$.getJSON();