ReactJS

Cette liste de bonne pratique est surtout un retour sur certaines "mauvaise pratiques" que nous avons mis dans notre code et comment les corriger.

Nombre de composants par fichiers composants

Un fichier "composant" ne DEVRAIT pas avoir plus d'un composant dedans :

/src/components/Menu/Content.js
function Label() {
return 'some label';
}
/src/components/Menu/Content.js
function Content() {
return 'some label';
}
/src/components/Menu/index.js
function Menu() {
return (
<div>
<div>
<Label />
</div>
<div>
<Content />
</div>
</div>
);
}
Intérêt

Le but est de trouver le bon composant quand on ouvre le fichier en question, en pas de trouver plusieurs composants sans trouver celui qui est censé être là.

Cas des "conteneurs" / "injecteurs"

Nommage de méthodes

Un méthode "répondant" à un évènement onSomething DEVRAIT être préfixée par handle.

class Foo() {
handleClick() {
// do something
}
render() {
return (
<button onClick={this.handleClick}>
Click
</button>
);
}
}

bind(this) dans la methode render

Le binding javascript n'est pas simple.

Les nouvelles fonctionnalités d'ECMAScript permettent de se rapprocher des autres langages et de ne plus avoir à connaitre les "internes", et c'est plutôt bien (même si c'est encore mieux de connaitre comment ça fonctionne sous le capôt).

Pour cette raison, le binding et les appels de méthodes DEVRAIENT être le plus "simple" et le plus "logique" pour les débutants.

Cela facilite aussi la lisibilité et la compréhension pour les développeurs chevronnés.

class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const foo = this.props.foo;
// do someting
}
render() {
return <button onClick={this.handleClick}></button>;
}
}

Composant "classe" : fonctions anonymes dans la methode render

De la même manière, on NE DOIT PAS créer des fonctions anonymes dans la méthode render.

Explications
  • Les fonctions anonymes sont regénérées à chaque appel de render, ce qui consomme de la ressource pour rien.
  • Cela rend la méthode render moins lisible car il y a du code "métier" dans le rendu.
class Foo extends PureComponent {
constructor(props) {
super(props);
this.handleButtonClick = this.handleButtonClick.bind(this);
this.state = {
foo: false,
};
}
handleButtonClick() {
this.setState((prevState) => ({
foo: !prevState.foo,
}));
}
render() {
return (
<div>
<button onClick={this.handleButtonClick}>Hey, click-me !</button>
</div>
);
}
}

Exception

La seule exception étant si l'on est en train d'itérer sur une liste et que l'on doit passer un object en paramètre.

On DOIT dans tous les cas limiter la fonction anonyme à un appel d'une autre méthode pour la lisibilité.

On PEUT alors faire comme ça:

class Foo extends PureComponent {
constructor(props) {
super(props);
this.handleButtonClick = this.handleButtonClick.bind(this);
this.state = {
btnClicked: null,
};
}
handleButtonClick(item) {
this.setState({
btnClicked: item,
});
}
render() {
return (<div>
{this.props.myList.map(item =>
<button onClick={(item) => this.handleButtonClick(item)}>
Hey, click-me !
</button>
}
</div>);
}
}

Hooks : définitions des fonctions de "setters"

À l'instar des composants de classe, on NE DEVRAIT PAS de définir des fonctions dans le corps du rendu de la méthode pour faire des actions simples:

function Container() {
const [isVisible, setIsVisible] = useState(false);
return <button onClick={() => setIsVisible(true)}>Make visible</button>;
}
Explication
  • Dans tous les cas la fonction est regénérée à chaque rendu
  • Le fait de définir une fonction supplémentaire peut complexifier la compréhension pour des cas simple
Attention

Il est par contre important d'extraire une fonction personnalisée quand le code devient complexe auquel cas la lisibilité du rendu du composant s'en trouverait diminuée.

Hooks : extraction de hooks personnalisés

On DEVRAIT extraire les hooks personnalisés dans les cas suivants :

  • Quand les "hooks" prennent plus de 40% de la taille du composant : extraire dans des hooks par "blocs fonctionnels"
  • Quand plusieurs "hooks" ont des "périmètres fonctionnels" différents, sortir les hooks par blocs fonctionnels différents

Il est très facile à extraire des hooks : il suffit de copier coller tout son code dans une autre fonction.

function useCurrentCart() {
const sdk = useSdk();
const { cartId } = useParams();
const [cart, setCart] = useState(null);
useEffect(() => {
sdk.find(cartId).then((cart) => {
setCart(cart);
});
}, [sdk, cartId]);
return cart;
}
function Cart() {
const cart = useCurrentCart();
if (cart === null) {
return <div>loading…</div>;
}
return (
<div>
<h2>{cart.title}</h2>
<p>
{cart.amount} {cart.currency}
</p>
</div>
);
}
Intérêt

Une meilleure lisibilité du composant, possibilité de tester les hooks.

Les hooks personnalisés PEUVENT être situés dans le fichier à côté du composant si ils restent petits et "privés".

Ils DEVRAIENT être dans un fichiers autonome dans le dossier du composant si ils sont "privés" mais complexes (plus de ~50 lignes).

Ils PEUVENT être situés dans un dossier / fichier partagés si ils sont utilisables par plusieurs composants. Dans ce cas ils DEVRAIENT être au plus près de leur utilisation métier. (un "hook" concernant uniquement les paniers devrait se trouver avec le code des paniers, et pas à la racine).

Hooks : useState multiples

On NE DEVRAIT PAS utiliser useState plusieurs fois autour du même "bloc fonctionnel", mais on DEVRAIT utiliser useReducer dans ce cas.

const initialState = {
isLoading: false,
cart: null,
error: null,
};
function reducer(state, action) {
switch (action.type) {
case 'FETCH_CART':
return {
isLoading: true,
cart: null,
error: null,
};
case 'CART_RECEIVED':
return {
isLoading: false,
cart: action.cart,
error: null,
};
case 'FETCH_CART_ERROR':
return {
isLoading: false,
cart: null,
error: action.error,
};
default:
throw new Error();
}
}
function useCurrentCart(sdk, cartId) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: 'FETCH_CART' });
sdk
.find(cartId)
.then((cart) => {
dispatch({ type: 'CART_RECEIVED', cart });
})
.catch((error) => {
dispatch({ type: 'FETCH_CART_ERROR', error });
});
}, [cartId]);
return state;
}
Intérêt
  • Pouvoir modifier facilement en ajoutant une clé, ou un cas sans avoir un "setter" qui est mal (ré-)initialisé.
  • Avoir la liste de tous les cas et les actions lisibles facilement sans avoir besoin de comprendre la logique sous-jacente.