Couleurs et psychologie : Créer une palette cohérente
Introduction La couleur est l'un des outils les plus puissants du design web. Elle influence…
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.
# 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
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
// src/components/Welcome.tsx
const Welcome = () => {
return (
# Bienvenue dans React !
Votre premier composant fonctionne.
);
};
export default Welcome;
// 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 = (
);
// 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 (
{avatar && (
)}
## {name}
Age: {age} ans
);
};
export default UserProfile;
// src/App.tsx
import UserProfile from './components/UserProfile';
function App() {
return (
# Notre équipe
);
}
export default App;
// Button.tsx
interface ButtonProps {
text: string;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
onClick?: () => void;
}
const Button = ({
text,
variant = 'primary',
disabled = false,
onClick
}: ButtonProps) => {
return (
}
disabled={disabled}
onClick={onClick}
>
{text}
);
};
// Utilisation
// Card.tsx
interface CardProps {
title: string;
children: React.ReactNode;
footer?: React.ReactNode;
}
const Card = ({ title, children, footer }: CardProps) => {
return (
### {title}
{children}
{footer && (
{footer}
)}
);
};
// Utilisation
}
>
Nom: Sophie Dupont
Email: sophie@example.com
// Input.tsx
interface InputProps extends React.InputHTMLAttributes {
label: string;
error?: string;
}
const Input = ({ label, error, ...inputProps }: InputProps) => {
return (
{error && {error}}
);
};
// Utilisation
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 (
## Compteur: {count}
);
};
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoList = () => {
const [todos, setTodos] = useState([]);
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 (
## Ma Todo List
setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="Nouvelle tâche..."
/>
{todos.map(todo => (
-
toggleTodo(todo.id)}
/>
{todo.text}
))}
{todos.filter(t => !t.completed).length} tâches restantes
);
};
interface FormData {
username: string;
email: string;
age: number;
newsletter: boolean;
}
const RegistrationForm = () => {
const [formData, setFormData] = useState({
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 (
);
};
const EventExamples = () => {
// Click
const handleClick = (e: React.MouseEvent) => {
console.log('Cliqué!', e.currentTarget);
};
// Input change
const handleChange = (e: React.ChangeEvent) => {
console.log('Valeur:', e.target.value);
};
// Form submit
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Formulaire soumis');
};
// Key press
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
console.log('Enter pressé');
}
};
// Focus
const handleFocus = (e: React.FocusEvent) => {
e.target.select(); // Sélectionne tout le texte
};
return (
);
};
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 (
{products.map(product => (
### {product.name}
{product.price}€
{/ Méthode 1 /}
{/ Méthode 2 (préférée) /}
))}
);
};
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 => (
-
{user.name}
{user.email}
))}
);
};
// ❌ Mauvaise pratique : index comme key
users.map((user, index) => (
{user.name} // Problèmes avec réorganisation
));
// ❌ Mauvaise pratique : pas de key
users.map(user => (
{user.name} // Warning React
));
const AdvancedUserList = () => {
const [users, setUsers] = useState([...]);
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 (
{sortedUsers.map(user => (
-
{user.name} - {user.email}
))}
Total: {sortedUsers.length} utilisateurs
);
};
const ConditionalRendering = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [user, setUser] = useState(null);
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
// Méthode 1: if/else
if (!isLoggedIn) {
return ;
}
// Méthode 2: Ternaire
return (
{isLoggedIn ? (
) : (
)}
);
// Méthode 3: && (pour un seul cas)
return (
{user && }
{error && }
);
// Méthode 4: Switch avec objet
const statusComponents = {
loading: ,
success: ,
error:
};
return (
{statusComponents[status]}
);
};
// 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([]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('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 Chargement...;
}
return (
# Recherche d'articles
setSearchTerm(e.target.value)}
className="search-input"
/>
{filteredArticles.length} résultat(s)
{filteredArticles.length === 0 ? (
Aucun article trouvé pour "{searchTerm}"
) : (
{filteredArticles.map(article => (
## {article.title}
{article.excerpt}
{article.category}
{article.tags.map(tag => (
#{tag}
))}
))}
)}
);
};
export default SearchApp;
// 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( );
expect(screen.getByText('Chargement...')).toBeInTheDocument();
});
test('affiche les articles après chargement', async () => {
render( );
await waitFor(() => {
expect(screen.getByText('Introduction à React')).toBeInTheDocument();
});
});
test('filtre les articles par recherche', async () => {
const user = userEvent.setup();
render( );
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( );
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();
});
});
/ 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;
}
handleClick, isLoading, hasErrorVous maîtrisez maintenant les trois piliers de React :
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.