Avant de connaitre les entrailles de React (parce que oui, pour faire du React vous n'avez pas vraiment le choix que de comprendre les entrailles de la bête sinon vous ferez erreur sur erreur…) on a tendance à se dire qu'un Function Component est une fonction est basta. Eh bien c'est faux ! 😵‍💫 Je vous explique ! 😉

Un composant fonction

Commençons par la base, un composant :

function Button({children}: React.PropsWithChildren) {
    return <button style={{ backgroundColor: 'red' }}>{children}</button>;
}

Ce composant n'a pas grand intérêt mais passons là-dessus, c'est un composant basic comme on en voit tous les jours. Et à l'usage rien de bien compliqué :

function Section() {
    return <section>
        <Button>Click me!</Button>
    </section>;
}

Jusque-là, je pense que vous ne devez pas être surpris. Mais On est d'accord que le composant Button est une fonction qui renvoie un fragment de JSX, donc je peux très bien écrire ça :

function Section2() {
    return <section>
        {Button({ children: 'Click me!' })}
    </section>;
}

C'est équivalent non ? 🤔 En tout cas, visuellement vous aurez le même résultat. Du coup c'est ok ? C'est ok hein ? 😰

Compilons le JSX…

Rappelons un élément basique de React : on passe par un compilateur pour le JSX, car ce n'est pas quelque chose de standard en JavaScript. Donc je vous propose de regarder la version compilée du JSX pour les différents morceaux de code de la partie précédente.

Note : je vais passer par le playground TypeScript pour compiler le code, on pourrait utiliser Babel ou autre, peu importe, le résultat serait le même. En tout cas vous pouvez re-tester par vous-même si vous voulez expérimenter vous-même.

function Button({ children }) {
    return React.createElement("button", { style: { backgroundColor: 'red' } }, children);
}

function Section() {
    return React.createElement("section", null,
        React.createElement(Button, null, "Click me!"));
}

function Section2() {
    return React.createElement("section", null, Button({ children: 'Click me!' }));
}

Vous pouvez remarquer que le composant Button a vu son JSX remplacé par un appel à React.createElement(). Simple ré-écriture du JSX sous forme d'appel de fonction. Pour ce qui est du composant Section, on voit 2 appels à React.createElement(), un premier pour la balise html section et un pour le children Button où on passe une référence vers la fonction de notre composant. Pour le composant Section2, on voit presque la même chose sauf qu'il n'y a qu'un seul appel à React.createElement() et directement l'appel au composant Button sous forme d'appel de fonction identique à la forme avec JSX qu'on avait plus haut.

Revenons sur la fonction React.createElement(). Cette fonction crée un contexte de composant React à partir de la référence de fonction qu'on lui passe, ce contexte va permettre l'isolation des hooks par exemples. On crée aussi un contexte qui permet à React de savoir quand il faut re-rendre le composant en se basant sur ses props. Donc passer par la version fonction casse complètement certaines optimisations naturelles de React…

On peut aussi arriver à des cas d'erreurs qui peut être parfois compliqué à comprendre dans une base de code plus grosse. Prenons ce code :


function Button({
  onClick,
  children,
}: React.PropsWithChildren<React.ComponentProps<'button'>>) {
  const [counter, setCounter] = useState(0);
  const backgroundColor = useMemo(
    () => (counter % 2 === 0 ? 'red' : 'green'),
    [counter]
  );
  const onClick = (e) =>
  return (
    <button
      onClick={(e) => {
        setCounter(counter + 1);
        onClick?.(e);
      }}
      style={{ backgroundColor }}
    >
      {children}
    </button>
  );
}

function Button2({
  onClick,
  children,
}: React.PropsWithChildren<React.ComponentProps<'button'>>) {
  const [color, setColor] = useState('R');
  return (
    <button
      onClick={(e) => {
        setColor(color === 'red' ? 'green' : 'red');
        onClick?.(e);
      }}
      style={{ backgroundColor: color }}
    >
      {children}
    </button>
  );
}


function Section2() {
  const [state, setState] = useState('CLICK_ME');
  if (state === 'CLICK_ME') {
    return (
      <section>
        {Button({ children: 'Click me!', onClick: () => setState('NOT_ME') })}
      </section>
    );
  } else {
    return (
      <section>
        {Button2({ children: 'No me!', onClick: () => setState('CLICK_ME') })}
      </section>
    );
  }
}

L'idée ici : on a un composant Section2 qui va afficher un composant Button ou un composant Button2, chacun utilise un nombre différent d'appel de hook (ou des appels de hooks différents) et on se retrouve avec une des erreurs suivante :

Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

ou

Rendered more hooks than during the previous render.

On a bien des sous composants mais comme on ne les appelle pas de façon à créer des contextes dissociés, React va empiler les appels de hook dans le contexte de Section2 et on arrive à des appels de hook conditionnel ce qui n'est pas possible en React.

Conclusion

React semble beaucoup plus simple que beaucoup d'autre framework frontend de prime abord, mais on se retrouve souvent avec des subtilités qui difficile à comprendre (voir des points tordus…). Je vous montre ici un exemple des "subtilités" de React qu'on découvre souvent plus tard après avoir passé quelque temps sur React et parfois quelques heures à se buter sur une erreur parlant de hook alors que ce n'est pas les hooks qui posent problèmes…

Pour résumé : n’appelez jamais vos composants comme des fonctions !

Sources :