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 ou logiquement liées dans des modules ou fonctions séparées.

astuce

Dans le développement d'applications React/Next.js, cette pratique est essentielle pour maintenir un code propre, lisible et facile à maintenir.

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 suit souvent ces patterns :

  • Extraction de la logique métier dans des hooks personnalisés
  • Séparation des composants UI en fichiers dédiés
  • Création de services/utilitaires pour les appels API
  • Organisation par fonctionnalités plutôt que par type de fichier

Exemple pratique

Voici un exemple simple de factorisation d'un composant React qui gère des données utilisateur :

// Avant 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 hooks personnalisés

Les hooks personnalisés permettent d'extraire et de réutiliser la logique d'état dans plusieurs composants. Ils commencent généralement par "use" et peuvent utiliser d'autres hooks React.

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

// 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 un hook personnalisé en React ?


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


Quel est le rôle d'un service API dans une application factorisée ?


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 suggérées

  1. Créer un hook personnalisé 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

Critères de réussite

  • Le code est correctement organisé en fichiers logiques
  • Le composant principal est plus simple et plus lisible
  • L'application fonctionne toujours comme avant
  • Les nouvelles fonctions et composants sont correctement typés avec TypeScript
Une solution

Voici une solution possible pour factoriser le code du fichier app/page.tsx :

1. Création d'un hook personnalisé 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 UI séparés

// components/QuizCard.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } 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 composant principal

// app/page.tsx
"use client";

import { useEffect, useState } from "react";
import { Alert, AlertTitle, AlertDescription } 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 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.