Aller au contenu
Frontend 7 min de lecture

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.

ReactAngularFrontendTypeScriptPerformanceTests

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

ReactAngular
NatureLibrairie UI + écosystèmeFramework complet
Courbe d’apprentissageDouce (JSX), puis complexe (state management)Raide (TypeScript + concepts Angular)
StructureLibre (choix du routing, state, etc.)Opiniâtrée (conventions imposées)
État globalZustand, Jotai, Redux Toolkit (à choisir)NgRx, Signals (intégrés depuis Angular 16)
Idéal pourSPAs, apps créatives, équipes React expérimentéesApps d’entreprise, grandes équipes, fort besoin de structure
Intégration AEMBonne (SPA Editor ou Headless)Bonne (SPA Editor ou Headless)
TestsJest + React Testing LibraryJasmine + Karma, ou Jest
Bundle sizeLé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

Amine MEGDICHE

Développeur AEM & Java Full Stack — Freelance depuis 2013

Vous avez un projet sur ces sujets ?

Envoyez-moi un message pour qualifier votre besoin, vos contraintes techniques et le bon format d'intervention.

Cadrer mon besoin