Fonctions JavaScript
Nous allons factoriser notre code et regrouper nos fonctions dans des fichiers dédiés
Qu’est-ce qu’une fonction JavaScript ?

Une fonction est un bloc de code réutilisable.
Une fonction peut contenir :
- de la logique (JavaScript/TypeScript)
- des paramètres pour personnaliser son comportement
- une valeur de retour (optionnelle)
Qu’est-ce que la factorisation de code ?
La factorisation de code est le processus consistant à extraire des parties de code réutilisables dans des fonctions séparées.
La factorisation permet de décomposer les gros composants en éléments plus petits et réutilisables.
Cela permet de réduire la duplication de code, d'améliorer la lisibilité et de faciliter la maintenance.

Quelle est la différence entre export et export default ?
La différence entre export function useQuiz() {} (sans default) et export default function QuizCard() {} (avec default) concerne la manière dont les modules sont exportés et importés en JavaScript/TypeScript (et donc en React/Next.js).
export (nommé)
// hooks/useQuiz.ts
export function useQuiz() {
// ...
}
- Export nommé : La fonction est exportée avec un nom explicite (
useQuiz) - Pour l'importer, tu dois utiliser exactement ce nom :
import { useQuiz } from '../hooks/useQuiz';
- Tu peux exporter plusieurs éléments nommés dans un même fichier :
export function useQuiz() {}
export function useAnotherHook() {}
export default
// components/QuizCard.tsx
export default function QuizCard(props) {
// ...
}
- Export par défaut : Le fichier exporte un seul élément principal, sans nom imposé à l'importation.
- À l'import, tu peux choisir le nom que tu veux :
import QuizCard from '../components/QuizCard';
- Tu ne peux avoir qu’un seul
export defaultpar fichier.
Pourquoi useQuiz est un export nommé et QuizCard/QuizResult sont des exports par défaut ?
useQuiz est un hook réutilisable :
- Il est logique de l’exporter par son nom (
useQuiz) pour que ce nom soit clair et cohérent dans tous les fichiers où il est utilisé. - Cela permet aussi d’exporter d’autres fonctions ou constantes du même fichier si besoin.
export function useQuiz() { ... }
export function useQuizTimer() { ... }
QuizCard et QuizResult sont des composants React :
- Chaque fichier contient un seul composant React.
- On utilise
export defaultcar c’est le composant principal du fichier. - C’est plus naturel et plus simple à importer dans d’autres composants :
import QuizCard from '@/components/QuizCard';
| Type d’export | Syntaxe | Importation | Cas d’usage typique |
|---|---|---|---|
| Export nommé | export function useQuiz() {} | import { useQuiz } from './useQuiz' | Fonctions utilitaires, hooks, constantes |
| Export par défaut | export default function Comp() | import Comp from './Comp' | Composant React |
Les principaux avantages de la factorisation sont :
- Réutilisabilité : Le code factorisé peut être utilisé à plusieurs endroits sans duplication
- Maintenabilité : Des fichiers plus petits et spécialisés sont plus faciles à comprendre et à modifier
- Testabilité : Des fonctions isolées sont plus simples à tester unitairement
- Collaboration : Une meilleure organisation facilite le travail en équipe
Dans une application Next.js avec TypeScript, la factorisation se fait souvent par :
- Extraction de la logique métier dans des fonctions personnalisés (appelées "hooks" en React)
- Séparation des composants de l'interface utilisateur (UI) en plusieurs fichiers
- Création de fonctions (services/utilitaires) pour les appels API
- Organisation par fonctionnalités plutôt que par type de fichier
Dans le développement d'applications, la factorisation est essentielle pour maintenir un code propre, lisible et facile à maintenir.
Exemple pratique
Voici un exemple simple de factorisation d'un composant React qui gère des données utilisateur :
// Avant la factorisation (composant monolithique)
export default function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Failed to fetch user');
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>Welcome, {user.name}!</div>;
}
// Après factorisation (hook personnalisé + composant simplifié)
// hooks/useUser.ts
export function useUser() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Failed to fetch user');
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUser();
}, []);
return { user, loading, error };
}
// components/UserProfile.tsx
export default function UserProfile() {
const { user, loading, error } = useUser();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>Welcome, {user.name}!</div>;
}
Quelques méthodes à connaître
1. Création de fonctions (hooks)
Les fonctions personnalisées (appelées hooks en React) permettent d'extraire et de réutiliser dans plusieurs composants des variables dont la valeur est conservée dans le temps (logique d'état).
Le nom d'un hook commence généralement par le mot use et peut utiliser d'autres hooks React.

Exemple simple de hook personnalisé useQuiz
// hooks/useQuiz.ts
import { useState, useEffect } from 'react';
export function useQuiz() {
const [questions, setQuestions] = useState([]);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [score, setScore] = useState(0);
const [isQuizCompleted, setIsQuizCompleted] = useState(false);
// Logique du quiz...
return {
questions,
currentQuestionIndex,
score,
isQuizCompleted,
setCurrentQuestionIndex,
setScore,
setIsQuizCompleted
};
}
2. Extraction des services API
Isoler les appels API dans des services dédiés améliore la séparation des préoccupations et facilite les tests.
// services/quizService.ts
import { supabase } from '../lib/supabaseClient';
export const quizService = {
async getQuestions() {
const { data, error } = await supabase
.from("question")
.select(`
id,
texte,
image_url,
image_credit_nom,
image_credit_url,
explication,
reponses:reponse (
id,
texte,
est_correcte
)
`)
.order("id", { ascending: true });
if (error) throw error;
return data;
},
async saveScore(userId: string, score: number, time: number) {
// Implémentation...
}
};
3. Composition de composants
Décomposer les composants complexes en éléments plus petits et réutilisables.
En informatique, la composition de composants signifie l'assemblage de composants.
// components/QuizCard.tsx
interface QuizCardProps {
question: string;
imageUrl?: string;
imageCredit?: string;
imageCreditUrl?: string;
answers: Answer[];
onAnswerSelect: (answer: Answer) => void;
showExplanation?: boolean;
explanation?: string;
}
export default function QuizCard({
question,
imageUrl,
imageCredit,
imageCreditUrl,
answers,
onAnswerSelect,
showExplanation,
explanation
}: QuizCardProps) {
// Implémentation du composant...
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Objectif
Factoriser le code du fichier app/page.tsx fourni pour améliorer son organisation et sa maintenabilité.
Instructions
- Analysez le code fourni et identifiez les parties qui pourraient être extraites dans des modules séparés
- Créez les fichiers nécessaires pour organiser le code de manière logique
- Refactorisez le composant principal pour utiliser ces nouveaux modules
- Assurez-vous que l'application fonctionne toujours correctement après la factorisation
Étapes
-
Créer une fonction personnalisée (hook) pour la logique du quiz
- Extrayez la logique de gestion des questions, du score et de l'état du quiz
- Créez un fichier
hooks/useQuiz.ts
-
Créer un service pour les interactions avec Supabase
- Extrayez les appels à Supabase dans un service dédié
- Créez un fichier
services/quizService.ts
-
Créer des composants UI séparés
- Identifiez les parties de l'UI qui pourraient être des composants séparés
- Créez les fichiers de composants nécessaires dans le dossier
components
-
Refactoriser le composant principal
- Simplifiez le fichier
app/page.tsxen utilisant les modules créés - Assurez-vous que toutes les fonctionnalités sont préservées
- Simplifiez le fichier
Corrigé
Pensez à créer une branche Git pour votre travail de refactorisation afin de pouvoir revenir en arrière si nécessaire.
git checkout -b factorisation
Une solution
Voici une solution possible pour factoriser le code du fichier app/page.tsx :
1. Création d'une fonction personnalisée (hook) pour la logique du quiz
// hooks/useQuiz.ts
import { useState, useEffect } from 'react';
import { quizService } from '../services/quizService';
export function useQuiz() {
const [questions, setQuestions] = useState<any[]>([]);
const [questionIndex, setQuestionIndex] = useState(0);
const [explication, setExplication] = useState("");
const [afficherExplication, setAfficherExplication] = useState(false);
const [score, setScore] = useState(0);
const [debut, setDebut] = useState<number | null>(null);
const [quizTermine, setQuizTermine] = useState(false);
const question = questions[questionIndex];
useEffect(() => {
async function fetchQuestion() {
const data = await quizService.getQuestions();
setQuestions(data || []);
setDebut(Date.now());
}
fetchQuestion();
}, []);
useEffect(() => {
if (questionIndex >= questions.length && questions.length > 0 && !quizTermine) {
setQuizTermine(true);
enregistrerMeilleurScore();
}
}, [questionIndex, questions.length, quizTermine]);
function handleClick(reponse: any) {
if (!question || afficherExplication) return;
const estBonneReponse = reponse.est_correcte;
if (estBonneReponse) {
setScore(prev => prev + 1);
}
const message = estBonneReponse
? "✅ Bonne réponse !"
: "❌ Mauvaise réponse.";
const explicationTexte = message + " " + question.explication || message;
setExplication(explicationTexte);
setAfficherExplication(true);
setTimeout(() => {
setAfficherExplication(false);
setExplication("");
setQuestionIndex((prev) => prev + 1);
}, 4000);
}
async function enregistrerMeilleurScore() {
const userId = localStorage.getItem("supabase_user_id");
if (!userId || debut === null || questions.length === 0) return;
const tempsTotal = Math.floor((Date.now() - debut) / 1000);
const scoreFinal = score;
const aujourdHui = new Date().toISOString().split("T")[0];
try {
await quizService.saveScore(userId, scoreFinal, tempsTotal, aujourdHui);
console.log("Score enregistré avec succès !");
} catch (error) {
console.error("Erreur lors de l'enregistrement du score :", error);
}
}
return {
questions,
question,
questionIndex,
explication,
afficherExplication,
score,
debut,
quizTermine,
handleClick
};
}
2. Création d'un service pour les interactions avec Supabase
// services/quizService.ts
import { supabase } from '../lib/supabaseClient';
export const quizService = {
async getQuestions() {
const { data, error } = await supabase
.from("question")
.select(`
id,
texte,
image_url,
image_credit_nom,
image_credit_url,
explication,
reponses:reponse (
id,
texte,
est_correcte
)
`)
.order("id", { ascending: true });
if (error) {
console.error("Erreur Supabase :", error);
throw error;
}
return data;
},
async getJoueur(userId: string) {
const { data, error } = await supabase
.from("joueur")
.select("pseudo, meilleur_score")
.eq("user_id", userId)
.single();
if (error) {
console.error("Erreur lors de la récupération du joueur :", error);
throw error;
}
return data;
},
async saveScore(userId: string, score: number, temps: number, date: string) {
// Récupérer le joueur et son ancien meilleur score
const { data: joueur, error } = await supabase
.from("joueur")
.select("meilleur_score")
.eq("user_id", userId)
.single();
if (error || !joueur) {
throw new Error("Impossible de récupérer les informations du joueur");
}
const ancienMeilleur = joueur.meilleur_score || 0;
// Mettre à jour seulement si nouveau record
if (score > ancienMeilleur) {
const { error: updateError } = await supabase
.from("joueur")
.update({
meilleur_score: score,
meilleur_temps: temps,
date_meilleur_score: date,
})
.eq("user_id", userId);
if (updateError) {
throw new Error("Erreur lors de la mise à jour du record");
}
console.log("Nouveau record !", score, "points en", temps, "s");
}
}
};
3. Création de composants d'interface utilisateur (UI) séparés
// components/QuizCard.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import Link from "next/link";
interface QuizCardProps {
question: any;
afficherExplication: boolean;
explication: string;
onAnswerClick: (reponse: any) => void;
}
export default function QuizCard({
question,
afficherExplication,
explication,
onAnswerClick
}: QuizCardProps) {
return (
<Card className="max-w-4xl mx-auto mt-6">
<div className="flex flex-col md:flex-row">
{/* Colonne gauche : image + crédit */}
<div className="w-full md:w-1/2 p-4">
{question.image_url ? (
<Image
src={question.image_url}
alt="Illustration de la question"
width={400}
height={300}
className="rounded"
/>
) : (
<div className="w-full h-[300px] bg-gray-100 flex items-center justify-center text-sm text-gray-500 rounded">
Aucune image disponible
</div>
)}
{question.image_credit_nom && question.image_credit_url && (
<Alert className="mt-4 text-sm text-muted-foreground">
<AlertDescription>
<span className="inline">
Image :{" "}
<Link
href={question.image_credit_url}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:text-primary"
>
{question.image_credit_nom}
</Link>
</span>
</AlertDescription>
</Alert>
)}
</div>
{/* Colonne droite : question + réponses */}
<div className="w-full md:w-1/2 p-4">
<CardHeader className="p-0 mb-4">
<CardTitle>Question</CardTitle>
</CardHeader>
<CardContent className="p-0">
<p className="text-lg font-semibold mb-4">{question.texte}</p>
{question.reponses.map((reponse: any) => (
<Button
key={reponse.id}
onClick={() => onAnswerClick(reponse)}
disabled={afficherExplication}
className="w-full justify-start mt-2 whitespace-normal text-left"
variant="outline"
>
{reponse.texte}
</Button>
))}
</CardContent>
{afficherExplication && (
<Alert className="mt-6 bg-yellow-50 border-yellow-300 text-yellow-800">
<AlertTitle>Explication</AlertTitle>
<AlertDescription>{explication}</AlertDescription>
</Alert>
)}
</div>
</div>
</Card>
);
}
// components/QuizResult.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Classement from "./Classement";
interface QuizResultProps {
score: number;
totalQuestions: number;
debut: number | null;
joueurNom: string;
}
export default function QuizResult({
score,
totalQuestions,
debut,
joueurNom
}: QuizResultProps) {
return (
<div className="text-center mt-20 max-w-2xl mx-auto">
<h2 className="text-4xl font-bold mb-8 text-primary">Quiz terminé !</h2>
<Card>
<CardHeader>
<CardTitle>Votre résultat</CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-xl">
<p>Score : <span className="font-bold text-green-600">{score}</span> / {totalQuestions}</p>
<p className="text-muted-foreground">
Temps : {debut ? Math.floor((Date.now() - debut) / 1000) : 0} secondes
</p>
{score === totalQuestions && (
<p className="text-2xl">Parfait ! 100% de bonnes réponses !</p>
)}
</CardContent>
</Card>
<div className="mt-8">
<p className="text-lg mb-4">
Merci {joueurNom} pour votre participation !
</p>
</div>
<Classement />
</div>
);
}
4. Refactorisation du fichier principal

// app/page.tsx
"use client";
import { useEffect, useState } from "react";
import { Alert, AlertTitle, AlertDescription, AlertTitle } from "@/components/ui/alert";
import FormulaireJoueur from "@/components/FormulaireJoueur";
import Score from "@/components/Score";
import QuizCard from "@/components/QuizCard";
import QuizResult from "@/components/QuizResult";
import { useQuiz } from "../hooks/useQuiz";
import { quizService } from "../services/quizService";
export default function Home() {
const [joueurNom, setJoueurNom] = useState("");
const [joueurPret, setJoueurPret] = useState(false);
const {
questions,
question,
questionIndex,
explication,
afficherExplication,
score,
debut,
quizTermine,
handleClick
} = useQuiz();
useEffect(() => {
if (joueurPret) {
const userId = localStorage.getItem("supabase_user_id");
if (userId) {
quizService.getJoueur(userId)
.then(data => {
if (data) {
setJoueurNom(data.pseudo);
}
})
.catch(error => {
console.error("Erreur lors de la récupération du joueur :", error);
});
}
}
}, [joueurPret]);
if (quizTermine) {
return (
<QuizResult
score={score}
totalQuestions={questions.length}
debut={debut}
joueurNom={joueurNom}
/>
);
}
return (
<div>
{!joueurPret ? (
<div>
<Alert className="bg-blue-50 border-blue-300 text-blue-800 max-w-xl mx-auto mt-6">
<AlertTitle className="text-xl font-semibold">Bienvenue sur CyberQuiz</AlertTitle>
<AlertDescription>
Un quiz pour tester vos connaissances en cybersécurité.
</AlertDescription>
</Alert>
<FormulaireJoueur onJoueurCree={() => setJoueurPret(true)} />
</div>
) : (
<div>
<Alert className="bg-green-50 border-green-300 text-green-800 max-w-xl mx-auto mt-6">
<AlertTitle className="text-xl font-semibold">
Bienvenue {joueurNom} !
</AlertTitle>
<AlertDescription>
Préparez-vous à tester vos connaissances en cybersécurité.
</AlertDescription>
</Alert>
<Score actuel={score} total={questions.length} />
{questions.length > 0 && question ? (
<QuizCard
question={question}
afficherExplication={afficherExplication}
explication={explication}
onAnswerClick={handleClick}
/>
) : (
<p className="text-center mt-6">Chargement de la question...</p>
)}
</div>
)}
</div>
);
}
Cette factorisation présente plusieurs avantages :
- Séparation des responsabilités : Chaque fichier a une responsabilité claire
- Réutilisabilité : Les composants et fonctions personnalisées (hooks) peuvent être réutilisés ailleurs
- Testabilité : Il est plus facile de tester chaque module individuellement
- Maintenabilité : Le code est plus facile à comprendre et à modifier
Le composant principal est maintenant beaucoup plus simple et se concentre sur l'assemblage des différents éléments plutôt que sur les détails d'implémentation.
Pensez à fusionner la branche Git factorisation dans votre branche principale.
git checkout main
git merge factorisation
