Débutant 12 min de lecture · 2 603 mots

Introduction à React : Composants, Props, et State

Introduction à React : Composants, Props, et State
Débutant · 2026.05.09

Pourquoi React ?

React est la bibliothèque JavaScript la plus populaire pour construire des interfaces utilisateur. En 2025, elle domine avec plus de 15 millions de téléchargements hebdomadaires sur NPM.

Avantages clés

  • Composants réutilisables : Créez une fois, utilisez partout
  • Virtual DOM : Mises à jour ultra-rapides
  • Écosystème riche : Milliers de bibliothèques compatibles
  • TypeScript natif : Support de premier ordre
  • React 18+ : Concurrent rendering et Suspense
  • 1. Premiers pas : Créer un projet React

    Avec Vite (Recommandé en 2025)

# Créer un nouveau projet
npm create vite@latest mon-app-react -- --template react-ts

# Naviguer et installer
cd mon-app-react
npm install

# Lancer le serveur de développement
npm run dev

Structure du projet

mon-app-react/
├── node_modules/
├── public/
├── src/
│   ├── App.tsx
│   ├── App.css
│   ├── main.tsx
│   └── vite-env.d.ts
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts

2. Composants : Les briques de votre application

Votre premier composant

// src/components/Welcome.tsx
const Welcome = () => {
  return (
    <div>
      # Bienvenue dans React !
      <p>Votre premier composant fonctionne.</p>
    </div>
  );
};

export default Welcome;

JSX : JavaScript + XML

// JSX permet d'écrire du HTML dans JavaScript
const element = # Hello, world!;

// Équivalent JavaScript pur (ne pas utiliser!)
const element = React.createElement('h1', null, 'Hello, world!');

// Expressions JavaScript dans JSX
const name = 'Marie';
const element = # Bonjour, {name}!;

// Attributs en camelCase
const button = (
  <button
    className="btn-primary"    // class → className
    onClick={handleClick}      // onclick → onClick
    disabled={isLoading}
  >
    Cliquez-moi
  </button>
);

Composant avec structure complète

// src/components/UserProfile.tsx
import { CSSProperties } from 'react';

interface UserProfileProps {
  name: string;
  age: number;
  avatar?: string;
}

const UserProfile = ({ name, age, avatar }: UserProfileProps) => {
  const cardStyle: CSSProperties = {
    border: '1px solid #ddd',
    borderRadius: '8px',
    padding: '20px',
    maxWidth: '300px',
    textAlign: 'center'
  };

  return (
    <div style={cardStyle}>
      {avatar && (
        <img
          src={avatar}
          alt={name}
          style={{ borderRadius: '50%', width: '100px', height: '100px' }}
        />
      )}
      ## {name}
      <p>Age: {age} ans</p>
    </div>
  );
};

export default UserProfile;

Utiliser votre composant

// src/App.tsx
import UserProfile from './components/UserProfile';

function App() {
  return (
    <div className="app">
      # Notre équipe
      <UserProfile
        name="Sophie Dupont"
        age={28}
        avatar="https://i.pravatar.cc/100?img=1"
      />
      <UserProfile
        name="Pierre Martin"
        age={32}
        avatar="https://i.pravatar.cc/100?img=2"
      />
      <UserProfile
        name="Julie Bernard"
        age={25}
      />
    </div>
  );
}

export default App;

3. Props : Passer des données aux composants

Props de base

// Button.tsx
interface ButtonProps {
  text: string;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  onClick?: () => void;
}

const Button = ({
  text,
  variant = 'primary',
  disabled = false,
  onClick
}: ButtonProps) => {
  return (
    <button
      className={btn btn--${variant}}
      disabled={disabled}
      onClick={onClick}
    >
      {text}
    </button>
  );
};

// Utilisation
<Button text="Enregistrer" variant="primary" />
<Button text="Annuler" variant="secondary" />
<Button text="Supprimer" variant="danger" onClick={handleDelete} />

Props.children : Contenu imbriqué

// Card.tsx
interface CardProps {
  title: string;
  children: React.ReactNode;
  footer?: React.ReactNode;
}

const Card = ({ title, children, footer }: CardProps) => {
  return (
    <div className="card">
      <div className="card-header">
        ### {title}
      </div>
      <div className="card-body">
        {children}
      </div>
      {footer && (
        <div className="card-footer">
          {footer}
        </div>
      )}
    </div>
  );
};

// Utilisation
<Card
  title="Mon profil"
  footer={<Button text="Sauvegarder" />}
>
  <p>Nom: Sophie Dupont</p>
  <p>Email: sophie@example.com</p>
</Card>

Props avec spread operator

// Input.tsx
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

const Input = ({ label, error, ...inputProps }: InputProps) => {
  return (
    <div className="form-group">
      <label>{label}</label>
      <input
        className={error ? 'input-error' : 'input'}
        {...inputProps}  // Spread toutes les props HTML natives
      />
      {error && <span className="error-message">{error}</span>}
    </div>
  );
};

// Utilisation
<Input
  label="Email"
  type="email"
  placeholder="votre@email.com"
  required
  maxLength={100}
  error="Email invalide"
/>

4. State : Données dynamiques et interactivité

useState : Le hook fondamental

import { useState } from 'react';

const Counter = () => {
  // [valeur, fonction pour modifier la valeur]
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(0);

  return (
    <div className="counter">
      ## Compteur: {count}
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

State avec types complexes

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

const TodoList = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputValue, setInputValue] = useState('');

  const addTodo = () => {
    if (inputValue.trim()) {
      const newTodo: Todo = {
        id: Date.now(),
        text: inputValue,
        completed: false
      };
      setTodos([...todos, newTodo]);
      setInputValue('');
    }
  };

  const toggleTodo = (id: number) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const deleteTodo = (id: number) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div className="todo-list">
      ## Ma Todo List

      <div className="todo-input">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="Nouvelle tâche..."
        />
        <button onClick={addTodo}>Ajouter</button>
      </div>

      <ul>
        {todos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
            <button onClick={() => deleteTodo(todo.id)}>×</button>
          </li>
        ))}
      </ul>

      <p>{todos.filter(t => !t.completed).length} tâches restantes</p>
    </div>
  );
};

State avec objets

interface FormData {
  username: string;
  email: string;
  age: number;
  newsletter: boolean;
}

const RegistrationForm = () => {
  const [formData, setFormData] = useState<FormData>({
    username: '',
    email: '',
    age: 18,
    newsletter: false
  });

  // ✅ Bonne pratique : fonction de mise à jour
  const updateField = (field: keyof FormData, value: any) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('Données du formulaire:', formData);
    // Envoyer à l'API...
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.username}
        onChange={(e) => updateField('username', e.target.value)}
        placeholder="Nom d'utilisateur"
      />

      <input
        type="email"
        value={formData.email}
        onChange={(e) => updateField('email', e.target.value)}
        placeholder="Email"
      />

      <input
        type="number"
        value={formData.age}
        onChange={(e) => updateField('age', parseInt(e.target.value))}
      />

      <label>
        <input
          type="checkbox"
          checked={formData.newsletter}
          onChange={(e) => updateField('newsletter', e.target.checked)}
        />
        Recevoir la newsletter
      </label>

      <button type="submit">S'inscrire</button>
    </form>
  );
};

5. Événements et handlers

Événements courants

const EventExamples = () => {
  // Click
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('Cliqué!', e.currentTarget);
  };

  // Input change
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log('Valeur:', e.target.value);
  };

  // Form submit
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log('Formulaire soumis');
  };

  // Key press
  const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      console.log('Enter pressé');
    }
  };

  // Focus
  const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
    e.target.select(); // Sélectionne tout le texte
  };

  return (
    <div>
      <button onClick={handleClick}>Cliquez</button>
      <input onChange={handleChange} onKeyPress={handleKeyPress} />
      <form onSubmit={handleSubmit}>
        <input onFocus={handleFocus} />
      </form>
    </div>
  );
};

Passer des paramètres aux handlers

const ProductList = () => {
  const products = [
    { id: 1, name: 'Laptop', price: 999 },
    { id: 2, name: 'Mouse', price: 29 },
    { id: 3, name: 'Keyboard', price: 79 }
  ];

  // Méthode 1: Arrow function inline (simple mais crée une nouvelle fonction à chaque render)
  const handleBuy1 = (productId: number) => {
    console.log('Achat produit:', productId);
  };

  // Méthode 2: Currying (plus performant)
  const handleBuy2 = (productId: number) => () => {
    console.log('Achat produit:', productId);
  };

  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          ### {product.name}
          <p>{product.price}€</p>

          {/* Méthode 1 */}
          <button onClick={() => handleBuy1(product.id)}>
            Acheter
          </button>

          {/* Méthode 2 (préférée) */}
          <button onClick={handleBuy2(product.id)}>
            Acheter
          </button>
        </div>
      ))}
    </div>
  );
};

6. Listes et clés

Rendu de listes

interface User {
  id: number;
  name: string;
  email: string;
  active: boolean;
}

const UserList = () => {
  const users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com', active: true },
    { id: 2, name: 'Bob', email: 'bob@example.com', active: false },
    { id: 3, name: 'Charlie', email: 'charlie@example.com', active: true }
  ];

  // ✅ Bonne pratique : key unique et stable
  return (
      {users.map(user => (
        <li key={user.id}>
          <span className={user.active ? 'active' : 'inactive'}>
            {user.name}
          </span>
          <span>{user.email}</span>
        
      ))}
  );
};

// ❌ Mauvaise pratique : index comme key
users.map((user, index) => (
  <li key={index}>{user.name}  // Problèmes avec réorganisation
));

// ❌ Mauvaise pratique : pas de key
users.map(user => (
- {user.name}  // Warning React
));

Listes filtrées et triées

const AdvancedUserList = () => {
  const [users, setUsers] = useState<User[]>([...]);
  const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('all');
  const [sortBy, setSortBy] = useState<'name' | 'email'>('name');

  // Filtrage
  const filteredUsers = users.filter(user => {
    if (filter === 'active') return user.active;
    if (filter === 'inactive') return !user.active;
    return true;
  });

  // Tri
  const sortedUsers = [...filteredUsers].sort((a, b) => {
    return a[sortBy].localeCompare(b[sortBy]);
  });

  return (
    <div>
      <div className="controls">
        <select value={filter} onChange={(e) => setFilter(e.target.value as any)}>
          <option value="all">Tous</option>
          <option value="active">Actifs</option>
          <option value="inactive">Inactifs</option>
        </select>

        <select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
          <option value="name">Nom</option>
          <option value="email">Email</option>
        </select>
      </div>

      <ul>
        {sortedUsers.map(user => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>

      <p>Total: {sortedUsers.length} utilisateurs</p>
    </div>
  );
};

7. Rendu conditionnel

Méthodes de rendu conditionnel

const ConditionalRendering = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [user, setUser] = useState<User | null>(null);
  const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');

  // Méthode 1: if/else
  if (!isLoggedIn) {
    return <LoginPage />;
  }

  // Méthode 2: Ternaire
  return (
    <div>
      {isLoggedIn ? (
        <Dashboard user={user} />
      ) : (
        <LoginPage />
      )}
    </div>
  );

  // Méthode 3: && (pour un seul cas)
  return (
    <div>
      {user && <Welcome name={user.name} />}
      {error && <ErrorMessage message={error} />}
    </div>
  );

  // Méthode 4: Switch avec objet
  const statusComponents = {
    loading: <LoadingSpinner />,
    success: <SuccessMessage />,
    error: <ErrorMessage />
  };

  return (
    <div>
      {statusComponents[status]}
    </div>
  );
};

8. Exemple complet : Application de recherche

// SearchApp.tsx
import { useState, useEffect } from 'react';

interface Article {
  id: number;
  title: string;
  excerpt: string;
  category: string;
  tags: string[];
}

const SearchApp = () => {
  const [articles, setArticles] = useState<Article[]>([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  const [loading, setLoading] = useState(true);

  // Simuler un fetch API
  useEffect(() => {
    const fetchArticles = async () => {
      setLoading(true);
      // Dans la vraie vie: await fetch('/api/articles')
      const mockArticles: Article[] = [
        {
          id: 1,
          title: 'Introduction à React',
          excerpt: 'Apprenez les bases de React',
          category: 'Tutorial',
          tags: ['react', 'javascript']
        },
        {
          id: 2,
          title: 'Next.js pour les débutants',
          excerpt: 'Créez des apps performantes',
          category: 'Tutorial',
          tags: ['nextjs', 'ssr']
        },
        {
          id: 3,
          title: 'TypeScript Best Practices',
          excerpt: 'Écrivez du TypeScript propre',
          category: 'Guide',
          tags: ['typescript', 'best-practices']
        }
      ];

      setTimeout(() => {
        setArticles(mockArticles);
        setLoading(false);
      }, 1000);
    };

    fetchArticles();
  }, []);

  // Filtrage
  const filteredArticles = articles.filter(article => {
    const matchesSearch = article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
                          article.excerpt.toLowerCase().includes(searchTerm.toLowerCase());
    const matchesCategory = selectedCategory === 'all' || article.category === selectedCategory;
    return matchesSearch && matchesCategory;
  });

  // Extraire les catégories uniques
  const categories = ['all', ...new Set(articles.map(a => a.category))];

  if (loading) {
    return <div className="loading">Chargement...</div>;
  }

  return (
    <div className="search-app">
      <header>
        # Recherche d'articles
      </header>

      <div className="filters">
        <input
          type="text"
          placeholder="Rechercher un article..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          className="search-input"
        />

        <select
          value={selectedCategory}
          onChange={(e) => setSelectedCategory(e.target.value)}
          className="category-select"
        >
          {categories.map(cat => (
            <option key={cat} value={cat}>
              {cat === 'all' ? 'Toutes catégories' : cat}
            </option>
          ))}
        </select>
      </div>

      <div className="results">
        <p>{filteredArticles.length} résultat(s)</p>

        {filteredArticles.length === 0 ? (
          <div className="no-results">
            Aucun article trouvé pour "{searchTerm}"
          </div>
        ) : (
          <div className="articles-grid">
            {filteredArticles.map(article => (
              <article key={article.id} className="article-card">
                ## {article.title}
                <p>{article.excerpt}</p>
                <div className="meta">
                  <span className="category">{article.category}</span>
                  <div className="tags">
                    {article.tags.map(tag => (
                      <span key={tag} className="tag">#{tag}</span>
                    ))}
                  </div>
                </div>
              </article>
            ))}
          </div>
        )}
      </div>
    </div>
  );
};

export default SearchApp;

9. Tests avec React Testing Library

// SearchApp.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchApp from './SearchApp';

describe('SearchApp', () => {
  test('affiche le loading initialement', () => {
    render(<SearchApp />);
    expect(screen.getByText('Chargement...')).toBeInTheDocument();
  });

  test('affiche les articles après chargement', async () => {
    render(<SearchApp />);

    await waitFor(() => {
      expect(screen.getByText('Introduction à React')).toBeInTheDocument();
    });
  });

  test('filtre les articles par recherche', async () => {
    const user = userEvent.setup();
    render(<SearchApp />);

    await waitFor(() => {
      expect(screen.getByText('Introduction à React')).toBeInTheDocument();
    });

    const searchInput = screen.getByPlaceholderText('Rechercher un article...');
    await user.type(searchInput, 'Next.js');

    expect(screen.getByText('Next.js pour les débutants')).toBeInTheDocument();
    expect(screen.queryByText('Introduction à React')).not.toBeInTheDocument();
  });

  test('filtre par catégorie', async () => {
    render(<SearchApp />);

    await waitFor(() => {
      expect(screen.getByText('Introduction à React')).toBeInTheDocument();
    });

    const categorySelect = screen.getByRole('combobox');
    fireEvent.change(categorySelect, { target: { value: 'Guide' } });

    expect(screen.getByText('TypeScript Best Practices')).toBeInTheDocument();
    expect(screen.queryByText('Introduction à React')).not.toBeInTheDocument();
  });
});

10. Styles CSS

/* SearchApp.css */
.search-app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.filters {
  display: flex;
  gap: 16px;
  margin: 24px 0;
}

.search-input,
.category-select {
  padding: 12px 16px;
  border: 2px solid #e2e8f0;
  border-radius: 8px;
  font-size: 16px;
  transition: border-color 0.2s;
}

.search-input {
  flex: 1;
}

.search-input:focus,
.category-select:focus {
  outline: none;
  border-color: #3b82f6;
}

.articles-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 24px;
  margin-top: 24px;
}

.article-card {
  padding: 24px;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  background: white;
  transition: transform 0.2s, box-shadow 0.2s;
}

.article-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}

.article-card h2 {
  margin: 0 0 12px 0;
  color: #1a202c;
}

.meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 16px;
}

.category {
  padding: 4px 12px;
  background: #e0e7ff;
  color: #4c51bf;
  border-radius: 12px;
  font-size: 14px;
}

.tags {
  display: flex;
  gap: 8px;
}

.tag {
  color: #718096;
  font-size: 14px;
}

.loading,
.no-results {
  text-align: center;
  padding: 48px;
  color: #718096;
  font-size: 18px;
}

Meilleures pratiques 2025

  • Toujours TypeScript : Typage = moins de bugs
  • Composants fonctionnels : Plus de class components
  • Props destructuring : Code plus lisible
  • Nommage explicite : handleClick, isLoading, hasError
  • Un composant = une responsabilité
  • Tests systématiques : Testing Library pour interactions utilisateur
  • Conclusion

    Vous maîtrisez maintenant les trois piliers de React :

  • Composants : Briques réutilisables
  • Props : Communication parent → enfant
  • State : Données dynamiques locales
  • Prochaines étapes

  • Pratiquez avec des mini-projets (todo, calculatrice, météo)
  • Explorez useEffect pour les side effects
  • Lisez l’article suivant : « NPM et Node.js : Gérer les dépendances »
  • Ressources

  • React Documentation – Nouvelle doc officielle
  • TypeScript React Cheatsheet
  • React Testing Library
  • À propos de l'auteur

    Pascal

    Pascal — SRE & Observability Engineer. J'applique les pratiques SRE (SLI/SLO, alerting, observabilité) à mon homelab Proxmox, avec les mêmes exigences qu'en production.

    Une remarque, un retour ?

    Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisis celui qui te convient.

    Laisser un commentaire