Retour au blog

Architecture React en production : patterns et bonnes pratiques

5 mars 202614 min

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 ?

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

← Tous les articlesAccueil