Aller au contenu principal

Fonctions JavaScript

Nous allons factoriser notre code et regrouper nos fonctions dans des fichiers dédiés

Qu’est-ce qu’une fonction JavaScript ?

Fonction

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.

astuce

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.

Factorisation

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 default par 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 default car 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’exportSyntaxeImportationCas d’usage typique
Export nomméexport function useQuiz() {}import { useQuiz } from './useQuiz'Fonctions utilitaires, hooks, constantes
Export par défautexport 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
attention

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.

useQuiz

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.

info

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


Quel est l'avantage principal de la factorisation de code ?


Comment nomme-t-on généralement une fonction personnalisée en React (hook) ?


Quelle approche est recommandée pour organiser les fichiers dans un projet Next.js ?


Qu'est-ce que la séparation des préoccupations (Separation of Concerns) ?



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

  1. Analysez le code fourni et identifiez les parties qui pourraient être extraites dans des modules séparés
  2. Créez les fichiers nécessaires pour organiser le code de manière logique
  3. Refactorisez le composant principal pour utiliser ces nouveaux modules
  4. Assurez-vous que l'application fonctionne toujours correctement après la factorisation

Étapes

  1. 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
  2. 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
  3. 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
  4. Refactoriser le composant principal

    • Simplifiez le fichier app/page.tsx en utilisant les modules créés
    • Assurez-vous que toutes les fonctionnalités sont préservées

Corrigé

astuce

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

Factorisation

// 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 :

  1. Séparation des responsabilités : Chaque fichier a une responsabilité claire
  2. Réutilisabilité : Les composants et fonctions personnalisées (hooks) peuvent être réutilisés ailleurs
  3. Testabilité : Il est plus facile de tester chaque module individuellement
  4. 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.

astuce

Pensez à fusionner la branche Git factorisation dans votre branche principale.

git checkout main
git merge factorisation

quiz_termine.png