Introduction
Passer d'un prototype React à une application en production demande une architecture réfléchie. Cet article présente les patterns et bonnes pratiques que j'utilise au quotidien en tant que développeur React senior.
Structure de dossiers
La structure de dossiers est le premier élément d'architecture. Voici celle que je recommande pour les projets de taille moyenne à grande :
src/
├── components/ # Composants réutilisables
│ ├── ui/ # Composants UI de base (Button, Input, Modal)
│ ├── forms/ # Composants de formulaire
│ └── layout/ # Composants de layout (Header, Footer, Sidebar)
├── features/ # Modules par fonctionnalité
│ ├── auth/ # Authentification
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types.ts
│ └── dashboard/ # Dashboard
│ ├── components/
│ ├── hooks/
│ └── types.ts
├── hooks/ # Hooks globaux
├── contexts/ # Contexts React
├── services/ # Services API
├── utils/ # Utilitaires
├── types/ # Types globaux
└── pages/ # Pages (routes)
Pourquoi cette structure ?
- Colocation : les fichiers liés sont proches les uns des autres
- Feature-based : facilite le travail en équipe et la suppression de features
- Scalable : fonctionne pour les petits comme les grands projets
Custom Hooks : la clé de la réutilisabilité
Les custom hooks sont l'outil le plus puissant de React pour la réutilisabilité et la séparation des préoccupations.
useAsync : gérer les opérations asynchrones
import { useState, useCallback } from 'react';interface AsyncState {
data: T | null;
loading: boolean;
error: string | null;
}
export function useAsync() {
const [state, setState] = useState>({
data: null,
loading: false,
error: null,
});
const execute = useCallback(async (asyncFn: () => Promise) => {
setState({ data: null, loading: true, error: null });
try {
const data = await asyncFn();
setState({ data, loading: false, error: null });
return data;
} catch (err) {
const message = err instanceof Error ? err.message : 'Erreur inconnue';
setState({ data: null, loading: false, error: message });
throw err;
}
}, []);
return { ...state, execute };
}
useDebounce : éviter les appels excessifs
import { useState, useEffect } from 'react';export function useDebounce(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Utilisation
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return setQuery(e.target.value)} />;
}
State Management
Quand utiliser quoi ?
| Outil | Cas d'usage |
| ------- | ------------- |
| useState | État local simple |
| useReducer | État local complexe avec de multiples actions |
| Context + useReducer | État partagé entre quelques composants proches |
| Zustand | État global simple et performant |
| TanStack Query | État serveur (données API) |
| Redux Toolkit | État global complexe avec beaucoup de logique |
Zustand : la simplicité avant tout
Zustand est devenu le choix privilégié pour le state management global grâce à sa simplicité :
import { create } from 'zustand';interface AuthStore {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}));
// Utilisation dans un composant
function ProfilePage() {
const { user, logout } = useAuthStore();
if (!user) return null;
return (
{user.name}
);
}
TanStack Query : l'état serveur
Pour les données provenant d'une API, TanStack Query (React Query) est incontournable :
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then((res) => res.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newUser: CreateUserInput) =>
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
}).then((res) => res.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
Optimisation des performances
React.memo : quand l'utiliser
N'utilisez React.memo que lorsqu'un composant se re-rend inutilement de manière mesurable :
// Bon usage : composant pur avec des props stables
const UserCard = React.memo(function UserCard({ user }: { user: User }) {
return (
{user.name}
{user.email}
);
});// Liste virtualisée pour de grandes quantités de données
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedList({ items }: { items: Item[] }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
{virtualizer.getVirtualItems().map((virtualItem) => (
{items[virtualItem.index].name}
))}
);
}
Code Splitting avec React.lazy
Le code splitting réduit le bundle initial et améliore le temps de chargement :
import { lazy, Suspense } from 'react';const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
}>
} />
} />
);
}
Error Boundaries
Les Error Boundaries capturent les erreurs de rendu et affichent un fallback :
import { Component, ReactNode } from 'react';interface Props {
children: ReactNode;
fallback: ReactNode;
}
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error) {
console.error('ErrorBoundary caught:', error);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Utilisation
Une erreur est survenue
}>
Conclusion
Une bonne architecture React repose sur des principes simples : colocation, séparation des préoccupations via les custom hooks, bon choix de state management et optimisations ciblées. Ces patterns sont le résultat de mon expérience en production et s'adaptent à la majorité des projets.
Vous cherchez un développeur React senior à Paris pour architecturer votre application ? Contactez-moi pour en discuter.