Développement React et Angular en 2025 : choisir et réussir son projet frontend
React vs Angular : quand choisir l'un ou l'autre, comment structurer un projet frontend professionnel, intégration avec AEM ou une API Spring Boot, performances et tests. Guide pour DSI et chefs de projet.
React ou Angular ? La question revient à chaque avant-projet frontend. Ce n’est pas une question de mode : les deux sont matures, maintenus et utilisés en production par des millions d’applications. Ce qui compte, c’est le contexte du projet, les compétences de l’équipe, et les contraintes d’intégration.
React vs Angular : les vraies différences
| React | Angular | |
|---|---|---|
| Nature | Librairie UI + écosystème | Framework complet |
| Courbe d’apprentissage | Douce (JSX), puis complexe (state management) | Raide (TypeScript + concepts Angular) |
| Structure | Libre (choix du routing, state, etc.) | Opiniâtrée (conventions imposées) |
| État global | Zustand, Jotai, Redux Toolkit (à choisir) | NgRx, Signals (intégrés depuis Angular 16) |
| Idéal pour | SPAs, apps créatives, équipes React expérimentées | Apps d’entreprise, grandes équipes, fort besoin de structure |
| Intégration AEM | Bonne (SPA Editor ou Headless) | Bonne (SPA Editor ou Headless) |
| Tests | Jest + React Testing Library | Jasmine + Karma, ou Jest |
| Bundle size | Léger (librairie seule : ~45KB gzip) | Plus lourd (framework complet : ~75KB gzip) |
Quand choisir React
- L’équipe a déjà une expérience React significative
- Le projet est une SPA avec des interactions riches et créatives
- Vous avez besoin de flexibilité sur les choix d’architecture
- L’application est destinée à être rendue côté serveur (Next.js)
- Intégration avec AEM en mode Headless ou SPA Editor
Quand choisir Angular
- L’équipe vient du monde Java/entreprise (Angular est plus familier avec ses services, injection de dépendances)
- Le projet est une application de gestion avec des formulaires complexes (Angular Forms est excellent)
- Vous avez plusieurs équipes qui travaillent sur le même codebase (les conventions imposées réduisent les divergences)
- Vous avez besoin d’une architecture Enterprise avec lazy loading de modules, guards, interceptors
React : structure d’un projet professionnel
src/
├── components/ # Composants réutilisables (aucune logique métier)
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx
│ │ └── Button.stories.tsx (Storybook)
├── features/ # Domaines métier (co-location des fichiers)
│ ├── commandes/
│ │ ├── CommandeList.tsx
│ │ ├── CommandeDetail.tsx
│ │ ├── commandeStore.ts (Zustand slice)
│ │ ├── commandeApi.ts (RTK Query endpoints)
│ │ └── commande.types.ts
├── hooks/ # Hooks custom réutilisables
├── services/ # Clients HTTP, utilitaires
├── store/ # Configuration du store global
└── pages/ # Routing (React Router ou TanStack Router)
Composant React moderne (avec hooks et TypeScript) :
// features/commandes/CommandeDetail.tsx
import { useQuery } from "@tanstack/react-query";
import { commandeApi } from "./commandeApi";
import type { Commande } from "./commande.types";
interface Props {
commandeId: string;
onClose: () => void;
}
export function CommandeDetail({ commandeId, onClose }: Props) {
const { data: commande, isLoading, error } = useQuery({
queryKey: ["commande", commandeId],
queryFn: () => commandeApi.getById(commandeId),
staleTime: 5 * 60 * 1000, // 5 minutes de cache
});
if (isLoading) return <CommandeDetailSkeleton />;
if (error) return <ErrorMessage error={error} />;
if (!commande) return null;
return (
<div role="dialog" aria-label={`Commande ${commande.numero}`}>
<header>
<h2>{commande.numero}</h2>
<StatutBadge statut={commande.statut} />
<button onClick={onClose} aria-label="Fermer">×</button>
</header>
<LignesCommande lignes={commande.lignes} />
<TotalCommande total={commande.total} />
</div>
);
}
Angular : structure et patterns Enterprise
src/
├── app/
│ ├── core/ # Services singleton, guards, interceptors
│ │ ├── auth/
│ │ ├── http/
│ │ └── error-handling/
│ ├── shared/ # Composants, pipes, directives partagés
│ │ ├── components/
│ │ └── pipes/
│ └── features/ # Modules fonctionnels (lazy-loaded)
│ ├── commandes/
│ │ ├── commandes.module.ts
│ │ ├── commandes-routing.module.ts
│ │ ├── commande-list/
│ │ └── commande-detail/
│ └── catalogue/
Service Angular avec Signals (Angular 17+) :
// core/auth/auth.service.ts
@Injectable({ providedIn: "root" })
export class AuthService {
private readonly http = inject(HttpClient);
private readonly router = inject(Router);
// Signals pour l'état réactif
currentUser = signal<User | null>(null);
isAuthenticated = computed(() => this.currentUser() !== null);
isAdmin = computed(() => this.currentUser()?.roles.includes("ADMIN") ?? false);
login(credentials: LoginRequest): Observable<void> {
return this.http.post<TokenResponse>("/api/v1/auth/login", credentials).pipe(
tap(response => {
localStorage.setItem("access_token", response.accessToken);
this.currentUser.set(this.decodeToken(response.accessToken));
}),
map(() => void 0)
);
}
logout(): void {
localStorage.removeItem("access_token");
this.currentUser.set(null);
this.router.navigate(["/login"]);
}
}
Intercepteur HTTP pour les tokens JWT :
// core/http/auth.interceptor.ts
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem("access_token");
if (token) {
req = req.clone({
headers: req.headers.set("Authorization", `Bearer ${token}`)
});
}
return next(req).pipe(
catchError(error => {
if (error.status === 401) {
inject(AuthService).logout();
}
return throwError(() => error);
})
);
};
Performance frontend : les métriques qui comptent
Core Web Vitals cibles :
- LCP < 2.5s — le plus grand élément visible au chargement
- INP < 200ms — réactivité aux interactions (remplace FID depuis 2024)
- CLS < 0.1 — stabilité visuelle
Optimisations systématiques :
// 1. Lazy loading des routes (React Router)
const Commandes = lazy(() => import("./features/commandes/CommandeList"));
// 2. Code splitting par feature (Angular)
{
path: "commandes",
loadChildren: () => import("./features/commandes/commandes.module")
.then(m => m.CommandesModule)
}
// 3. Virtualisation des longues listes
import { FixedSizeList } from "react-window";
<FixedSizeList height={600} itemCount={10000} itemSize={80}>
{({ index, style }) => <CommandeRow commande={commandes[index]} style={style} />}
</FixedSizeList>
// 4. Mémoïsation (React)
const total = useMemo(
() => lignes.reduce((acc, l) => acc + l.prix * l.quantite, 0),
[lignes]
);
Intégration avec Spring Boot / AEM
React + Spring Boot API (fetch avec gestion des tokens) :
// services/api.ts
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
});
api.interceptors.request.use(config => {
const token = localStorage.getItem("access_token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) authStore.getState().logout();
return Promise.reject(error);
}
);
Intégration AEM Headless (React) :
// Utilisation du SDK AEM Headless pour les Content Fragments
import AEMHeadless from "@adobe/aem-headless-client-js";
const aemClient = new AEMHeadless({
serviceURL: process.env.REACT_APP_AEM_URL,
endpoint: "/content/graphql/global/endpoint.json",
auth: [process.env.REACT_APP_AEM_USER, process.env.REACT_APP_AEM_PASSWORD]
});
const { data } = await aemClient.runQuery(`
query GetArticles($path: String!) {
articleList(filter: { _path: { _expressions: [{ value: $path }] } }) {
items { titre description image { _path } }
}
}
`, { path: "/content/dam/monsite/articles" });
Tests frontend
React — Testing Library (comportement utilisateur, pas implémentation) :
describe("CommandeDetail", () => {
it("affiche le statut et les lignes quand la commande est chargée", async () => {
const commande = buildCommande({ statut: "LIVREE", lignes: 3 });
render(<CommandeDetail commandeId={commande.id} onClose={vi.fn()} />, {
wrapper: QueryClientWrapper
});
// L'utilisateur voit le bon statut
expect(await screen.findByText("LIVREE")).toBeInTheDocument();
// Et toutes les lignes
expect(screen.getAllByRole("row")).toHaveLength(3 + 1); // +1 header
});
});
Que vous partiez sur React ou Angular, la clé est la cohérence : une architecture claire, des conventions respectées par toute l’équipe, et des tests qui valident le comportement visible par l’utilisateur — pas les détails d’implémentation.
Si vous avez un projet frontend à lancer ou une application existante à reprendre, contactez-moi pour en discuter.
Amine MEGDICHE
Développeur AEM & Java Full Stack — Freelance depuis 2013