Aller au contenu principal

Cours 5 - Plusieurs composants

Utiliser plusieurs composants permettra de « naviguer sur plusieurs pages Web » et permettra également d'éviter de répéter des portions de page Web similaires.

🐣 Créer un nouveau composant

Pour créer un composant, c'est très simple : il suffit de créer un nouveau fichier page.tsx dont le code initial correspondra généralement à ceci :

'use client'; // Seulement nécessaire pour les composants interactifs

export default function NomDuComposant() {

return (
<div>Ceci est le composant NomDuComposant !</div>
);
}
attention

Pour le nom de la fonction principale d'un composant, la convention à respecter est PascalCase. (C'est-à-dire qu'on commence avec une majuscule et chaque nouveau mot commence par une majuscule)

  • export default function nomDuComposant()
  • export default function NomDuComposant()

Mais où crée-t-on ce fichier page.tsx ? Il y en a déjà un avec ce nom dans le dossier app.

La réponse est située dans les sections ♻ Composants réutilisables et 🚗 Routage, plus bas. Cela dépendra de comment on souhaite utiliser le composant.

Il y a deux manières d'utiliser et d'intégrer un composant au site Web :

  1. Composants réutilisables : afficher un composant spécifique dans un autre composant spécifique. Pratique pour réutiliser un affichage répétitif.
  2. 🚗 Routage : afficher un composant différent selon la route (l'URL) actuelle. Pratique pour créer une « illusion de navigation entre les pages Web ».

🚗 Routage

Ci-dessous, on peut observer la navigation entre plusieurs composants. Selon la route (chemin après le domaine dans la barre d'URL) actuelle, un composant différent est intégré dans la page parmi les composants blue, red, yellow et pink.

Navigation entre plusieurs composants

📂 Routage basé sur les répertoires

Avec Next.js, le routage des pages Web est basé sur la structure des dossiers du projet.

Exemples :

  • Le chemin localhost:3000/blue affiche le composant Blue car il est situé dans le dossier /app/blue.
  • Le chemin localhost:3000/red affiche le composant Red car il est situé dans le dossier /app/red.
  • Le chemin racine ( localhost:3000 ) affiche le composant Home car il est situé dans le dossier /app.
Composants et routage

Quelques remarques :

  • 🌳 Le composant situé directement dans le dossier app est celui associé au chemin racine ( localhost:3000 ) et s'affiche immédiatement lorsqu'on ouvre l'application Web.
  • 🌿 On peut créer des sous-dossiers à volonté. (Comme le composant LightBlue, situé dans le dossier /app/blue/light-blue )
  • 🌌 Certains dossiers peuvent être vides (On peut avoir un composant avec la route localhost:3000/options/langue et n'en avoir aucun avec la route localhost:3000/options. Le dossier /app/options ne contiendrait aucun fichier page.tsx, mais le dossier /app/options/langue contiendrait un fichier page.tsx.)
astuce

🐞 Lorsque vous ajoutez des dossiers et déplacez des composants dans votre projet pour manipuler le routage, redémarrez l'exécution (npm run dev) du projet, car changer la structure du projet cause généralement des erreurs durables.

attention

Pour les noms des dossiers, la convention à respecter est kebab-case. (C'est-à-dire pas de majuscules : on sépare les mots par des traits d'union !)

  • ❌ LightBlue
  • ❌ lightBlue
  • ❌ light_blue
  • ✅ light-blue
Convention kebab-case

🔲 Layouts et menus de navigation

Et si on souhaite qu'une partie du HTML soit commune à toutes les pages Web ? (Menu de navigation, header, footer, etc.)

Les fichiers layout.tsx sont fait pour ça.

Layout global

Ci-dessus, on a un layout global (utilisé pour toutes les pages du site Web) défini dans le fichier layout.tsx qui est situé dans le dossier app.

On remarque d'ailleurs un menu de navigation qui fonctionne à l'aide d'éléments <Link> :

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<nav>
<Link className="home" href="/">Accueil</Link>
<Link className="blue" href="/blue">Bleu</Link>
<Link className="red" href="/red">Rouge</Link>
<Link className="yellow" href="/yellow">Jaune</Link>
<Link className="pink" href="/pink">Rose</Link>
</nav>
{children} {/* ◀◀◀ Le composant sera chargé ici ! */}
<footer>Ceci est un fouteur</footer>
</body>
</html>
);
}
info

Gardez à l'esprit que le composant associé à la route sera intégré à l'endroit où on place l'instruction {children}.

🔳 Layout supplémentaire pour certains composants

Si on préfère que certaines pages aient un layout supplémentaire EN PLUS du layout racine, on peut ajouter un fichier layout.tsx dans un autre dossier. Tous les composants de ce dossier et de ses sous-dossiers seront intégré à ce nouveau layout.

Layout supplémentaire

Le code pour un layout pourrait ressembler à ceci :

export default function BlueLayout( {children}: Readonly<{children: React.ReactNode }> ) {

return(
<div>
<nav>
<Link className="blue" href="/blue">Bleu</Link>
<Link className="lightBlue" href="/blue/light-blue">Bleu pâle</Link>
</nav>
{children} { /* ◀◀◀ Intégration des composants */}
<footer>Fouteur supplémentaire</footer>
</div>
)

}

Quelques remarques :

  • Il ne faut surtout pas oublier d'intégrer {children} quelque part, sinon le composant Blue et les potentiels composants dans ses sous-dossiers ne seront pas affichés.
  • Ce layout lui-même sera intégré dans le layout racine, où {children} était placé.

On peut voir le résultat, visuellement, pour les composants Blue et LightBlue :

Layout supplémentaire

🖼 Utiliser un layout différent pour certains composants

Le layout racine est essentiel à l'affichage car il contient certains éléments indispensables comme <html>, <head> et <body>. On ne peut pas afficher un composant sans l'intégrer au layout racine.

Ce qu'on peut faire, cela dit, est de séparer nos pages Web par layout. Dans ce cas-ci, nous aurons deux groupes :

  1. Les pages qui utilisent le menu de navigation principal.
  2. Les pages qui utilisent le menu de navigation limité aux composants Blue et LightBlue.

👻 Étape 1 : Alléger le layout racine au maximum

On conserve uniquement les éléments indispensables, comme <html>, <head> et <body>. Il n'y a aucun élément visible dans ce layout. Le <nav> et le <footer> précédemment présents seront déplacés dans un autre layout.

export default function RootLayout({ children }: Readonly<{ children : React.ReactNode }>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
{children} {/* Le composant ou layout sera chargé ici ! */}
</body>
</html>
);
}

📂 Étape 2 : Séparer les composants en deux groupes

Grâce à des dossiers encadrés de parenthèses, qui seront ignorés par le routage, on sépare les composants en deux groupes.

Chaque groupe possède son propre layout, déclaré dans le dossier parent du groupe. (Ici, (root) et (blue))

Plusieurs layouts indépendants

Quelques remarques :

  • Aucun des chemins (Ex : http://localhost:3001/blue) n'a changé ! Les dossiers entourés de parenthèses sont invisibles pour le routage.
  • Le composant utilisé pour la page d'accueil doit être situé dans (root) OU dans (blue), mais pas les deux ! S'il y avait un fichier page.tsx dans chacun de ces deux dossiers, ils auraient la même route, et cela lancerait une erreur.

On a maintenant des composants qui utilisent exclusivement le layout root ou le layout blue :

Layout principal

🚁 Routes dynamiques

Parfois, on souhaite utiliser des paramètres dans la route. Cela signifie qu'une partie de la route est variable (dynamique).

Exemple :

  • localhost:3000/list/character/1
  • localhost:3000/list/character/4
  • localhost:3000/list/character/12

Bien entendu, le composant qui sera chargé par cette route devra récupérer ce paramètre de route (qui, ici, vaut 1, 4 ou 12 dans ces exemples) pour l'utiliser. (Par exemple, faire une requête à un API Web pour obtenir des données associées au nombre obtenu)

Pour créer une route dynamique, on doit créer un dossier encadré de crochets :

Route dynamique

On peut ensuite y ajouter un composant, comme d'habitude, avec un fichier page.tsx.

La route pour le composant dans le dossier [id] sera http://localhost:3000/red/ID_DE_NOTRE_CHOIX. Par exemple, http://localhost:3000/red/2 fonctionnerait et chargerait le composant.

Pour récupérer la valeur associé au paramètre de route nommé id, on pourrait procéder comme ceci :

'use client';

import { useParams } from "next/navigation";

export default function RedId() {

// Conteneur pour tous les paramètres de route (Dans ce cas, on a juste « id »)
const params = useParams<{ id : string }>();

return (
<div>
<h2>🔴 Composant rouge/{params.id} 🔴</h2>
<div className="red big">L'id reçue est {params.id}</div>
</div>
);
}
Paramètre de route
remarque

Les paramètres de route sont toujours des string. (Même si ça ressemble à un nombre, c'est un string qui passe par l'URL)

attention

Si jamais votre paramètre de route contient des espaces ou certains caractères spéciaux et que vous tenter de l'afficher dans le HTML, vous pourriez obtenir quelque chose comme Mon%20param%E8tre%20de%20route. Pour corriger cela, utilisez simplement decodeURIComponent(params.id) en l'affichant.

🛴 Routes dynamiques optionnelles

Disons qu'on souhaite qu'un paramètre de route soit optionnel, c'est-à-dire qu'une page (un composant) est accessible même si on ne fournit pas le paramètre de route demandé, alors il faudra qu'on utilise un « Optional Catch-all Segment ».

Les 🗄 Catch-All Segment sont similaires aux routes dynamiques, à quelques différences près :

  1. Le nom du dossier ressemblera plutôt à [...params] plutôt que [param]. (Remarquez les ...)
  2. On peut glisser plusieurs paramètres de route plutôt qu'un seul.

Par exemple, si la structure des dossiers est app/red/[...params]/page.tsx, alors toutes les routes suivantes seront valides :

  • http://localhost:3000/red/allo
  • http://localhost:3000/red/a/b/c
  • etc.

Les ❔ Optional Catch-All Segments sont similaires aux 🗄 Catch-All Segments, à quelques différences près :

  1. Le nom du dossier ressemblera plutôt à [[...params]] plutôt que [...params]. (Remarquez les doubles crochets)
  2. On peut glisser de 0 à plusieurs paramètres de routes.

Par exemple, si la structure des dossiers est app/red/[[...params]]/page.tsx, alors toutes les routes suivantes seront valides :

  • http://localhost:3000/red
  • http://localhost:3000/red/allo
  • http://localhost:3000/red/a/b/c
  • etc.

Voici comment récupérer les paramètres de routes avec un ❔ Optional Catch-All Segment dont le dossier est nommé [[...proute]] :

export default function MyComponent() {

// ⛔ Attention ! proute est un tableau de string !
const params = useParams<{ proute : string[] }>();

useEffect(() => {

// Attention ! params.proute est peut-être undefined ! (Si aucun paramètre de route n'a été fourni)
const proute = params.proute;

if(proute != undefined){

// Ouf ! On peut tenter d'aller récupérer des paramètres, mais on ne sait pas il y en a combien !
const param1 = proute[0]; // Existe forcément

const param2 = proute[1]; // Pourrait être undefined...
const param3 = proute[2]; // Pourrait être undefined...

}

// ...

}, []);

// ...
}

(params a été manipulé dans useEffect(), mais ça aurait pu être dans n'importe quelle fonction !)

attention

Pour les 🗄 Catch-All Segment et les ❔ Optional Catch-All Segment, il est impossible d'ajouter des dossiers enfants supplémentaires puisque ces segments peuvent déjà contenir une infinité de paramètres de route.

Pour les ❔ Optional Catch-All Segment, le dossier parent ne peut pas contenir de page.tsx ! Puisque le paramètre de route est optionnel, disons qu'on a la structure de dossiers app/red/[[...params]]/page.tsx, alors http://localhost:3000/red ET http://localhost:3000/red/allo mèneront vers le même page.tsx, qui sera glissé dans le dossier [[...params]].

🚧 Rediriger l'utilisateur

Si jamais on désire rediriger l'utilisateur (par exemple, après s'être connecté ou après avoir rempli un formulaire), on peut procéder comme ceci dans une fonction de notre choix :

// Attention d'utiliser cette importation et non next/router !
import { useRouter } from "next/navigation";

export default function Red() {

// Objet permettant de manipuler la route
const router = useRouter();

function someFunction(){
router.push("/red/1"); // Redirige vers une autre route
}

// ...
}

♻ Composants réutilisables

Ci-dessous, on peut apercevoir trois sections / composants :

Composant réutilisable

Le composant ItemView est un composant réutilisable qui peut être intégré dans n'importe quel autre composant. C'est très utile s'il y a une section du HTML qu'on a besoin de réutiliser dans plusieurs pages Web.

📌 Afficher un composant dans un autre composant

Disons qu'on souhaite afficher le composant ItemView quelque part dans le composant Yellow, comme ceci :

Composant réutilisable

📝 Étapes à suivre

  1. Créer le composant dans un dossier nommé _components
Dossier pour les composants réutilisables
'use client';

export default function ItemView() {

return(
<div className="item">
<h3>Composant ItemView</h3>
<p>Affichage d'un item</p>
</div>
);

}
attention

Le trait de soulignement au début du nom du dossier _components est indispensable pour que ce dossier ne soit pas pris en compte par le routage. C'est ici qu'on rangera tous nos composants réutilisables. Comme il y aura plusieurs composants dans ce dossier, leur fichier peut être nommé avec quelque chose de plus descriptif que page.tsx, mais n'oubliez pas de respecter la covenvention kebab-case.

  1. Intégrer le composant réutilisable

Dans un autre composant de notre choix (ou dans plusieurs !), il suffit d'ajouter un élément HTML dont le nom correspond à la fonction principale de notre composant réutilisable :

'use client';

// Importation du composant réutilisable
import ItemView from "@/app/_components/item-view";

export default function Yellow() {

return (
<div>
<h2>🟢 Composant jaune 🟢</h2>
<div className="green big">
<p>Bienvenue dans le composant jaune.</p>
<ItemView /> {/* ◀◀◀ Intégration du composant réutilisable */}
</div>
</div>
);
}

🕊 Passer un paramètre à un composant

On veut généralement transmettre une ou plusieurs données du composant parent vers le composant enfant. (Des données qui seront utilisées dans l'affichage, par exemple)

📝 Étapes à suivre

  1. Spécifier un paramètre qui servira de conteneur pour le ou les paramètres du composant

Ci-dessous, le composant ItemView possède un paramètre props, dont le type est un objet anonyme qui possède une propriété itemNo et une propriété itemName. (Dont leur type est spécifié)

'use client';

export default function ItemView(props : {itemNo : number, itemName : string}) {

return(
<div className="item">
<h3>Composant ItemView</h3>
<p>Item #{props.itemNo} : {props.itemName}</p>
</div>
);

}

On remarque d'ailleurs que props.itemNo et props.itemName sont utilisés dans le HTML du composant.

remarque

Le paramètre conteneur n'est pas obligé d'être nommé props, mais c'est une convention.

info

Contrairement aux paramètres de route, les paramètres de composant peuvent avoir n'importe quel type puisqu'ils ne sont pas passés dans l'URL.

  1. Depuis le composant parent, fournir des valeurs pour le(s) paramètre(s)

Ci-dessous, on voit que l'élément <ItemView /> a été complété avec deux attributs qui serviront à envoyer la valeur 1 pour la propriété itemNo et la valeur 'Chaise' pour la propriété itemName. (Les apostrophes '' encadrent le mot « Chaise » car c'est un string)

'use client';

import ItemView from "@/app/_components/item-view";

export default function Yellow() {

return (
<div>
<h2>🟢 Composant jaune 🟢</h2>
<div className="green big">
<p>Bienvenue dans le composant jaune.</p>
<ItemView itemNo={1} itemName={'Chaise'} /> {/* itemNo et itemName sont remplis ! */}
</div>
</div>
);
}

Observez le résultat dans le HTML :

Paramètres envoyés au composant enfant

🍇 Intégration multiple

Rien ne nous empêche d'afficher plusieurs fois un composant réutilisable ! Voici un exemple :

Code du composant parent :

'use client';

import ItemView from "@/app/_components/item-view";
import { Item } from "@/app/_types/item";

export default function Yellow() {

// Liste d'items à afficher
const items : Item[] = [
new Item(1, "Chaise"),
new Item(2, "Pneu"),
new Item(3, "Cheval")
];

return (
<div>
<h2>🟢 Composant jaune 🟢</h2>
<div className="green big">
<p>Bienvenue dans le composant jaune.</p>
{/* Affichage multiple du composant ItemView à l'aide de la liste d'items et de .map() */}
{items.map(
(i) => <ItemView key={i.id} itemNo={i.id} itemName={i.name} />
)}
</div>
</div>
);
}
Affichage répétitif d&#39;un composant réutilisable

💩 L'enfer de Tailwind

Comme vous savez, parfois, en utilisant Tailwind, le HTML peut finir par ressembler à ceci :

<button className="bg-blue-500 text-white py-2 px-4 rounded-sm font-bold" onClick={incrementX}>Incrémenter X</button>

L'ensemble de classes peut sembler monstrueux, mais il faut avoir des réflexes stratégiques : il faut convertir ce bouton en un composant qui contient uniquement ce bouton ! De cette manière, au lieu de copier-coller toutes ces classes pour chaque bouton, on va simplement intégrer un composant réutilisable qui contient uniquement ce bouton. Ça peut être vrai également pour un input, un menu, une image, etc. !

En général, visez une des deux visions suivantes :

  • 👨‍🎨 Créez votre propre CSS à 100%.
  • ♻ Utilisez Tailwind à 95% et créez de nombreux mini-composants réutilisables. (Quelques classes personnalisées peuvent être acceptables, rarement)

Exemple de composant pour ce bouton :

export default function BlueButton(props : {text : string, fct : any}){

return(

<button className="bg-blue-500 text-white py-2 px-4 rounded-sm font-bold" onClick={props.fct}>
{props.text}
</button>

);

}

Remarquez que le texte a été reçu en paramètre, mais aussi une fonction du composant parent ! Cela permet d'ajouter un événement onClick sur le bouton facilement.

🎨 Modules CSS

Bien que le fichier globals.css soit pratique, parfois on pourrait vouloir créer des styles qui s'appliquent seulement à certains composants plutôt qu'au projet en entier. Les modules CSS permettent de le faire.

🐣 Créer un module CSS

Le nom du fichier doit avoir la forme ____.module.css en respectant la convention kebab-case.

Il y a deux endroits cohérents où on peut ranger ce fichier, selon l'usage de votre module CSS :

  • Le module servira à plusieurs composants variés : On range le module dans app/_styles.
  • Le module servira à un composant précis : On range le module dans le dossier du composant. (Sauf si c'est un composant réutilisable, alors là, on peut mettre le module CSS dans app/_styles finalement)
Répertoire pour les modules CSS

Le fichier contiendra simplement des styles, par exemple :

.test{

color:fuchsia;

}
attention

Les modules CSS ne peuvent pas cibler de simples éléments. (Sélecteurs purs) Il faut absolument utiliser des classes ! (ou des ids)

/* ✅ Valide car c'est une classe */ 
.test{
color:fuchsia;
}

/* ⛔ Invalide car c'est un sélecteur pur */
p{
margin:0px;
}

/* ✅ Valide car implique une classe */
p.large{
width:1000px;
padding:10px;
}

Avant de crier au scandale que les modules CSS sont trop limités, gardez à l'esprit que l'usage de sélecteurs purs, en général, n'est pas idéal car cela cible généralement trop d'éléments de manière potentiellement imprévisible et conflictuelle.

📬 Importer un module CSS

Dans le ou les composants de votre choix, importez un ou plusieurs modules CSS comme ceci, par exemple :

'use client';

// Importation d'un module CSS
import styles from '../../_styles/tables.module.css';

export default function Pink() {

return (
<div>
<h2>🌸 Composant rose 🌸</h2>
<div className="pink big">Bienvenue dans le composant rose.</div>
<p className={styles.test}>Du texte fuchsia</p> {/* Usage de la classe .test dans le module tables.module.css */}
</div>
);
}

Quelques remarques :

  • Il faut (malheureusement) préciser styles.nomDeMaClasse pour pouvoir utiliser une classe du module CSS. Cependant, cela permet d'avoir des classes avec le même nom dans d'autres modules CSS.
  • Le mot styles qui a été utilisé dans l'instruction import aurait pu être n'importe quoi d'autre. (Ex : styles1) N'hésitez pas à utiliser des noms variés si vous voulez importer plusieurs modules CSS.
  • Notez que les classes .pink et .big viennent de globals.css. C'est pour ça qu'elles ne sont pas précédées de styles.
  • Si vous souhaitez utiliser un id de votre module CSS, la syntaxe sera id={styles.nomDeMonId}.
astuce

Si jamais vous souhaitez combiner plusieurs classes de votre modules CSS et / ou plusieurs classes déclarées dans globals.css, ça pourrait ressembler à ceci :

<p className={styles.test + ' ' + styles.ghost + ' ' + 'big'}>Du texte fuchsia</p>

On a donc utilisé la concaténation pour jumeler les classes .test et .ghost du module CSS ainsi que la classe .big de globals.css.

Si vous préférez les template strings, c'est à peine plus élégant :

<p className={`${styles.test} ${styles.ghost} big`}>Du texte fuchsia</p>
remarque

Les auteurs de la librairie Tailwind déconseillent l'usage de modules CSS. Le but de Tailwind est d'offrir des classes qui ont le même comportement peu importe le navigateur et la page Web. En créant des styles dont la portée est exclusive à certaines pages, les classes Tailwind peuvent devenir imprévisibles.

De toute façon, tel que mentionné plus haut, soit vous utilisez votre propre CSS à 100%, soit vous utilisez Tailwind à 95% et n'aurez même pas besoin des modules CSS de toute façon.