👨‍💻 Un Tech Lead fullstack pour accélérer votre projet ?
Je suis dispo pour des missions React / Node / Cloud.

Blog

  • Comment construire une stack multi-cloud souveraine sans exploser vos coûts ?

    🌩️ Le multi-cloud, un luxe ou nécessité ?

    Dans un monde où la souveraineté numérique, la résilience et la scalabilité sont devenues des enjeux stratégiques, de plus en plus d’entreprises se tournent vers une stratégie multi-cloud. Objectif : éviter la dépendance à un seul fournisseur (vendor lock-in), optimiser les performances et garantir la disponibilité des services critiques.
    Mais très vite, un piège se referme : l’explosion des coûts, la complexité d’orchestration, la duplication des ressources

    Comment faire dans ce cas ?

    🧱 Étape 1 — L’architecture modulaire : poser une fondation portable

    « Chaque équipe utilisait son cloud préféré. Rien n’était compatible. Le jour où on a voulu migrer un service de staging vers la prod, tout a cassé. »

    Beaucoup d’équipes commencent en mono-cloud, avec les outils maison du provider. Ça fonctionne… jusqu’au jour où on veut :

    • changer de cloud pour une question de coût ou souveraineté,
    • déployer dans un autre pays,
    • ou isoler des services sensibles.

    Si ton stack est verrouillé sur un provider, t’es piégé.

    💡 Solution : standardiser ton socle technique pour qu’il soit portable :

    • Conteneurisation (Docker, Podman) → packager tout de la même façon.
    • Orchestrateur commun (Kubernetes, Nomad) → une seule logique de déploiement.
    • Infrastructure as Code (Terraform, Pulumi) → tout est versionné, réutilisable.

    Résultat ? Tu peux répliquer un environnement complet sur un autre cloud en quelques heures. Sans tout réécrire.


    🧰 Étape 2 — Choisir ses clouds avec une logique de mission

    « Pourquoi notre frontend est sur Azure, le backend sur AWS et la base chez OVH ? — Personne ne savait. »

    Le multi-cloud n’a de sens que si chaque service est à sa place :

    • stockage froid sur un provider éco,
    • IA sur un cloud hyper spécialisé,
    • data sensible sur un hébergeur souverain ou auto-hébergé.

    C’est pas une question de techno. C’est une décision de bon sens, motivée par :

    • la sécurité,
    • les coûts,
    • la conformité.

    Et comme on dit toujours, pas tous les oeufs dans le même panier.


    💡 Étape 3 — Penser FinOps dès le début

    « On pensait que le staging coûtait 100€/mois. La facture : 480. Une base oubliée, deux volumes jamais supprimés, et des instances qui tournaient la nuit. »

    Dans le multi-cloud, les coûts s’éparpillent. Tu ne les vois pas venir.
    Penser FinOps, ce n’est pas juste installer un outil de suivi.
    C’est :

    • estimer les coûts avant chaque déploiement,
    • automatiser l’extinction des environnements non critiques,
    • éviter la duplication inutile (ex: pas 3 bases SQL pour 3 microservices).

    En intégrant ces réflexes tôt, tu évites les mauvaises surprises.
    Et surtout, tu gardes la main. Pas de factures en souffrance.


    🔐 Étape 4 — Sécurité : c’est toi le garant

    « Notre stockage objet était public. Par défaut. Et personne ne l’avait vu. »

    Chaque cloud a sa façon de gérer les accès, les secrets, les logs…
    Et ça rend le multi-cloud dangereux si tu ne centralises pas ta sécurité.

    Une stack multi-cloud doit avoir :

    • un point unique de gestion des secrets,
    • des identités gérées via un SSO ou OIDC,
    • une politique claire de journalisation et d’audit.

    Tu ne peux pas tout vérifier à la main. Tu dois t’outiller, documenter, automatiser.

    La souveraineté, ce n’est pas juste choisir un cloud français.
    C’est assumer la responsabilité de tes données, partout.


    📡 Étape 5 — Supervision et contrôle : savoir, pas espérer

    « Un jour, un cluster K3s est tombé. Personne n’a été alerté. Les logs n’étaient pas centralisés. On a passé 2h à chercher ce qui se passait. »

    Dans un système distribué, c’est ta visibilité qui fait ta résilience.
    S’il te manque un dashboard, un log, un backup, tu es aveugle. Et vulnérable.

    C’est pourquoi dès le départ, il faut :

    • superviser tous les clusters et clouds dans un seul outil (Grafana, Datadog, etc.),
    • agréger les logs et les erreurs dans un endroit unique,
    • automatiser les backups entre les clouds, et tester leur restauration.

    Pas besoin d’un gros budget. Juste d’une stratégie claire.
    Et d’une règle simple : aucune brique ne doit être orpheline.

    Envie d’un audit gratuit de votre stack actuelle ou de discuter stratégie multi-cloud ?

  • Replacing Developers with AI: Engineering Failure or Civilizational Shift?

    Since the dawn of technology, we’ve built tools to transcend our limitations.
    Computing is perhaps the most emblematic of these efforts. From machines capable of executing only the most basic mathematical instructions, we crafted entire layers of abstraction — languages, frameworks, operating systems — that allowed us to harness raw power through human-readable logic.

    Programming languages, at their core, are a translation.
    They turn the unfathomable speed of silicon into something that the human mind can model, predict, and build upon.

    But now, we’re tempted to remove that layer of human intervention.
    Not because it has become obsolete.
    But because it has become too slow.

    Engineers as the bottleneck

    Let’s face it — we, the engineers, are no longer fast enough.
    Not fast enough for the market. Not scalable enough for the investors. Not efficient enough for the production pipelines we ourselves helped build.

    And so, enter AI.

    Language models. Intelligent agents.
    These tools promise to eliminate the slowest element in the feedback loop: us.
    They can generate code. Deploy infrastructure. Analyze and refactor logic.

    From a purely mechanical standpoint, it’s genius.
    From a systemic standpoint, it’s dangerous.

    The illusion of meta-optimization

    Replacing a human developer with an AI doesn’t optimize the system.
    It optimizes the optimization process.

    Instead of improving the way we build software, we’re now building systems that simulate the process of building software — faster, cheaper, and with no regard for the subtleties that made engineering a discipline in the first place.

    It’s a form of recursion: automation for automation’s sake.

    But here’s the catch:
    Producing working code is not the same as delivering value.
    Code is just the surface. Beneath it lie trade-offs, ethics, sustainability, and responsibility — none of which are encoded in the output of a language model.

    Not yet. And certainly not by default.

    Acceleration ≠ Progress

    The desire to go faster is understandable.
    But faster does not mean further.

    History shows us that meaningful progress is not linear.
    It comes from iteration, reflection, divergence.
    From the uncomfortable slowness of deliberate thought.

    If we accelerate blindly, we risk reinforcing broken systems.
    Automating technical debt. Scaling inefficiency.
    Creating fragility at global scale — wrapped in the illusion of innovation.

    This is not speculative fiction.
    It’s already happening.

    A dangerous decision

    So let’s be clear:
    Replacing developers with AI is not just a technical shift.
    It’s a strategic one. A political one. A civilizational one.

    The question is no longer “can we automate this?”
    It’s “should we?”

    And it’s not a question for developers.
    It’s a question for decision-makers.
    For those who shape policy, allocate capital, influence direction.

    Choosing to eliminate the human from the software loop may feel like pragmatism.
    But it’s not.
    It’s short-termism, disguised as innovation.

    We won’t gain time.
    We’ll lose meaning.

    Human-enhanced, not human-replaced


    AI is a revolution.
    And like every revolution, it comes with responsibility.

    Used wisely, AI can support engineers.
    It can reduce friction. Automate the boring. Assist in complexity.

    But it should not become the engineer.
    Not yet. Maybe not ever.

    Because the value of engineering is not just in what we build — but in how, and why.

    If we abandon that, we’re not just replacing workers.
    We’re abandoning ownership.
    And eventually, we’ll lose trust in the very systems we depend on.

    Final thoughts: an alarm, not a rejection

    This article isn’t a rejection of AI.
    I’m an engineer. A builder. A techno-optimist.

    But I also believe in human-centered technology.
    In purposeful innovation.
    In building systems that serve humanity — not replace it.

    Automate the tools.
    Streamline the processes.
    But keep the humans where it matters: at the helm.

    Not to control the AI.
    But to remind ourselves what we’re building for in the first place.

  • Comment bien découpler l’état, la logique métier et les vues en React Native

    Dans 90 % des projets React Native mal architecturés, le problème vient toujours du même endroit : tout est collé dans le composant.

    Photo de Markus Spiske sur Unsplash


    Quand les hooks, les appels d’API, les mutations d’état et le rendu JSX cohabitent dans un même fichier, tu perds le contrôle. Le code devient impossible à tester, à maintenir, à faire évoluer. Tu veux éviter ça ? Lis ce qui suit.

    Votre stack vous ralentit ?

    Je vous propose un audit technique gratuit pour identifier les freins à la performance ou à la maintenabilité.

    1. Comprendre les 3 couches fondamentales

    Pour bien structurer une application React Native, il faut séparer clairement :

    • La vue (UI) : ce que l’utilisateur voit (composants React).
    • La logique métier (Use Cases) : ce que l’application fait (règles, traitements, appels d’API).
    • L’état (State Management) : ce que l’application sait (données locales ou distantes).

    2. Exemple simple : la todo app mal fichue

    ❌ Ne fais surtout pas ça
    const TodoScreen = () => {
      const [todos, setTodos] = useState([]);
      const [loading, setLoading] = useState(false);
    
      useEffect(() => {
        setLoading(true);
        fetch('https://api/todos')
          .then(res => res.json())
          .then(setTodos)
          .finally(() => setLoading(false));
      }, []);
    
      return (
        <View>
          {loading ? <ActivityIndicator /> : todos.map(todo => <Text>{todo.title}</Text>)}
        </View>
      );
    };
    
    
    
    
    
    

    Problèmes :

    • Pas testable
    • Mélange de logique métier, état et vue
    • Difficulté à faire évoluer ce code (pagination, filtre, etc.)

    3. 🔨 Solution : découplage intelligent

    a. La Vue → composants purs

    Elle ne fait que recevoir des props et déclencher des callbacks.

    const TodoList = ({ todos, loading }) => (
      <View>
        {loading ? <ActivityIndicator /> : todos.map(todo => <Text key={todo.id}>{todo.title}</Text>)}
      </View>
    );
    
    

    b. La logique métier → use case ou hook métier

    b. La logique métier → use case ou hook métier
    
    // useCases/useFetchTodos.ts
    export const useFetchTodos = () => {
      const [todos, setTodos] = useState([]);
      const [loading, setLoading] = useState(false);
    
      useEffect(() => {
        const fetchTodos = async () => {
          setLoading(true);
          const res = await fetch('https://api/todos');
          const data = await res.json();
          setTodos(data);
          setLoading(false);
        };
        fetchTodos();
      }, []);
    
      return { todos, loading };
    };
    

    c. L’état → context, Zustand, Jotai, Redux Toolkit

    Ici on reste local, mais si les todos sont utilisés ailleurs, il faut extraire l’état global dans un store partagé.

    4. Bonus : structuration du projet

    /src
      /components
        TodoList.tsx        ← UI pure
      /hooks
        useFetchTodos.ts    ← logique métier
      /screens
        TodoScreen.tsx      ← composition
      /stores
        todos.store.ts      ← état global (si nécessaire)
    

    Conclusion:

    Tu veux scaler un projet React Native ? Alors sépare.
    Si tu ne peux pas expliquer en 5 secondes où est la logique métier d’un écran, c’est que tu as échoué à architecturer ton app.

    Découvrez comment utiliser l’architecture hexagonale avec ReactJS

  • Implémentation d’une architecture hexagonale avec React et Redux

    Photo de Jonas Svidras sur Unsplash

    L’architecture hexagonale, également connue sous le nom de « Ports et Adaptateurs », est une approche de conception logicielle qui vise à créer des applications flexibles, maintenables et testables. Elle sépare clairement les préoccupations entre le domaine métier, l’infrastructure et l’interface utilisateur.

    Dans ce tutoriel, nous allons explorer comment structurer une application React avec Redux en utilisant une architecture hexagonale. Nous prendrons l’exemple d’une gestion d’utilisateurs pour illustrer les concepts, en suivant la structure de projet détaillée ci-dessous.

    Votre stack vous ralentit ?

    Je vous propose un audit technique gratuit pour identifier les freins à la performance ou à la maintenabilité.

    Structure du projet

    Voici la structure du projet que nous allons utiliser :

    app/                # Application : spécifique à React et Redux
      components/
      features/
        user/
          userSlice.ts
          userSelectors.ts
          userThunks.ts
          userHooks.ts
          pages/
            UserList.tsx
            UserDetail.tsx
            UserForm.tsx
      layout/
        FullPage.tsx
        Dashboard.tsx
      routes/
        userRoutes.ts
      store/
        index.ts
    domain/             # Métier : types et interfaces métiers
      entities/
        User.ts
      ports/
        UserRepository.ts
      services/
        UserService.ts
    infrastructure/     # Adapteurs : API, stockage, etc.
      api/
        userApi.ts
      storage/
        userStorage.ts
    styles/
    utils/
    

    Domaine (domain/)

    Le domaine contient la logique métier pure de votre application. Il est indépendant de l’interface utilisateur et de l’infrastructure.

    1. Entités (entities/)

    domain/entities/User.ts

    export interface User {
      id: string;
      name: string;
      email: string;
      role: string;
    }
    

    2. Ports (ports/)

    Les ports sont des interfaces qui définissent comment le domaine interagit avec l’extérieur.

    domain/ports/UserRepository.ts

    import { User } from '../entities/User';
    
    export interface UserRepository {
      getUsers(): Promise<User[]>;
      getUserById(id: string): Promise<User | null>;
      createUser(user: User): Promise<User>;
      updateUser(user: User): Promise<User>;
      deleteUser(id: string): Promise<void>;
    }
    

    3. Services (services/)

    Les services contiennent la logique métier et utilisent les ports pour accéder aux données.

    domain/services/UserService.ts

    import { User } from '../entities/User';
    import { UserRepository } from '../ports/UserRepository';
    
    export class UserService {
      constructor(private userRepository: UserRepository) {}
    
      async getAllUsers(): Promise<User[]> {
        // Logique métier supplémentaire si nécessaire
        return await this.userRepository.getUsers();
      }
    
      async getUserById(id: string): Promise<User | null> {
        // Logique métier supplémentaire si nécessaire
        return await this.userRepository.getUserById(id);
      }
    
      async createUser(user: User): Promise<User> {
        // Exemple de validation métier
        if (!user.email.includes('@')) {
          throw new Error('Email invalide');
        }
        return await this.userRepository.createUser(user);
      }
    
      async updateUser(user: User): Promise<User> {
        // Logique métier pour la mise à jour
        return await this.userRepository.updateUser(user);
      }
    
      async deleteUser(id: string): Promise<void> {
        // Logique métier pour la suppression
        return await this.userRepository.deleteUser(id);
      }
    }
    

    Infrastructure (infrastructure/)

    L’infrastructure contient les implémentations concrètes des ports définis dans le domaine.

    1. API (api/)

    infrastructure/api/userApi.ts

    import axios from 'axios';
    import { User } from '../../domain/entities/User';
    import { UserRepository } from '../../domain/ports/UserRepository';
    
    export class UserApi implements UserRepository {
      private apiUrl = '/api/users';
    
      async getUsers(): Promise<User[]> {
        const response = await axios.get<User[]>(this.apiUrl);
        return response.data;
      }
    
      async getUserById(id: string): Promise<User | null> {
        const response = await axios.get<User>(`${this.apiUrl}/${id}`);
        return response.data;
      }
    
      async createUser(user: User): Promise<User> {
        const response = await axios.post<User>(this.apiUrl, user);
        return response.data;
      }
    
      async updateUser(user: User): Promise<User> {
        const response = await axios.put<User>(`${this.apiUrl}/${user.id}`, user);
        return response.data;
      }
    
      async deleteUser(id: string): Promise<void> {
        await axios.delete(`${this.apiUrl}/${id}`);
      }
    }
    
    
    
    
    
    
    

    2. Stockage (storage/)

    Optionnellement, vous pouvez implémenter un stockage local.

    infrastructure/storage/userStorage.ts

    import { User } from '../../domain/entities/User';
    import { UserRepository } from '../../domain/ports/UserRepository';
    
    export class UserStorage implements UserRepository {
      private storageKey = 'users';
    
      async getUsers(): Promise<User[]> {
        const data = localStorage.getItem(this.storageKey);
        return data ? JSON.parse(data) : [];
      }
    
      async getUserById(id: string): Promise<User | null> {
        const users = await this.getUsers();
        return users.find(user => user.id === id) || null;
      }
    
      async createUser(user: User): Promise<User> {
        const users = await this.getUsers();
        users.push(user);
        localStorage.setItem(this.storageKey, JSON.stringify(users));
        return user;
      }
    
      async updateUser(user: User): Promise<User> {
        let users = await this.getUsers();
        users = users.map(u => (u.id === user.id ? user : u));
        localStorage.setItem(this.storageKey, JSON.stringify(users));
        return user;
      }
    
      async deleteUser(id: string): Promise<void> {
        let users = await this.getUsers();
        users = users.filter(u => u.id !== id);
        localStorage.setItem(this.storageKey, JSON.stringify(users));
      }
    }
    

    Application (app/)

    L’application gère l’interface utilisateur avec React et la gestion d’état avec Redux.

    1. Store Redux (store/)

    app/store/index.ts

    import { configureStore } from '@reduxjs/toolkit';
    import userReducer from '../features/user/userSlice';
    
    export const store = configureStore({
      reducer: {
        user: userReducer,
        // Autres reducers...
      },
    });
    
    export type RootState = ReturnType<typeof store.getState>;
    export type AppDispatch = typeof store.dispatch;
    

    app/store/hooks.ts

    import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
    import type { RootState, AppDispatch } from './index';
    
    // Utilisez ces hooks personnalisés dans votre application
    export const useAppDispatch = () => useDispatch<AppDispatch>();
    export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
    

    2. Features (features/)

    Nous allons maintenant détailler la partie features/user/.

    a. Slice utilisateur (userSlice.ts)

    app/features/user/userSlice.ts

    import { createSlice } from '@reduxjs/toolkit';
    import { User } from '../../domain/entities/User';
    import { fetchUsers, fetchUserById, createUser, updateUser, deleteUser } from './userThunks';
    
    interface UserState {
      users: User[];
      selectedUser: User | null;
      loading: boolean;
      error: string | null;
    }
    
    const initialState: UserState = {
      users: [],
      selectedUser: null,
      loading: false,
      error: null,
    };
    
    const userSlice = createSlice({
      name: 'user',
      initialState,
      reducers: {
        clearSelectedUser(state) {
          state.selectedUser = null;
        },
      },
      extraReducers: builder => {
        builder
          // fetchUsers
          .addCase(fetchUsers.pending, state => {
            state.loading = true;
            state.error = null;
          })
          .addCase(fetchUsers.fulfilled, (state, action) => {
            state.loading = false;
            state.users = action.payload;
          })
          .addCase(fetchUsers.rejected, (state, action) => {
            state.loading = false;
            state.error = action.error.message || 'Erreur lors du chargement des utilisateurs';
          })
          // fetchUserById
          .addCase(fetchUserById.pending, state => {
            state.loading = true;
            state.error = null;
          })
          .addCase(fetchUserById.fulfilled, (state, action) => {
            state.loading = false;
            state.selectedUser = action.payload;
          })
          .addCase(fetchUserById.rejected, (state, action) => {
            state.loading = false;
            state.error = action.error.message || "Erreur lors du chargement de l'utilisateur";
          })
          // createUser
          .addCase(createUser.fulfilled, (state, action) => {
            state.users.push(action.payload);
          })
          // updateUser
          .addCase(updateUser.fulfilled, (state, action) => {
            const index = state.users.findIndex(u => u.id === action.payload.id);
            if (index !== -1) {
              state.users[index] = action.payload;
            }
          })
          // deleteUser
          .addCase(deleteUser.fulfilled, (state, action) => {
            state.users = state.users.filter(u => u.id !== action.payload);
          });
      },
    });
    
    export const { clearSelectedUser } = userSlice.actions;
    export default userSlice.reducer;
    

    b. Thunks (userThunks.ts)

    Modification importante : Les thunks utilisent maintenant les services du domaine au lieu d’accéder directement à l’API.

    app/features/user/userThunks.ts

    import { createAsyncThunk } from '@reduxjs/toolkit';
    import { User } from '../../domain/entities/User';
    import { UserService } from '../../domain/services/UserService';
    import { UserApi } from '../../infrastructure/api/userApi';
    
    // Instanciez le service avec l'implémentation du repository
    const userRepository = new UserApi();
    const userService = new UserService(userRepository);
    
    export const fetchUsers = createAsyncThunk('user/fetchUsers', async () => {
      return await userService.getAllUsers();
    });
    
    export const fetchUserById = createAsyncThunk('user/fetchUserById', async (id: string) => {
      return await userService.getUserById(id);
    });
    
    export const createUser = createAsyncThunk('user/createUser', async (user: User) => {
      return await userService.createUser(user);
    });
    
    export const updateUser = createAsyncThunk('user/updateUser', async (user: User) => {
      return await userService.updateUser(user);
    });
    
    export const deleteUser = createAsyncThunk('user/deleteUser', async (id: string) => {
      await userService.deleteUser(id);
      return id;
    });
    

    c. Sélecteurs (userSelectors.ts)

    Les sélecteurs permettent d’accéder facilement aux données du store.

    app/features/user/userSelectors.ts

    import { RootState } from '../../store';
    
    export const selectUsers = (state: RootState) => state.user.users;
    export const selectSelectedUser = (state: RootState) => state.user.selectedUser;
    export const selectUserLoading = (state: RootState) => state.user.loading;
    export const selectUserError = (state: RootState) => state.user.error;
    

    d. Hooks (userHooks.ts)

    Les hooks personnalisés facilitent la réutilisation de la logique dans les composants.

    app/features/user/userHooks.ts

    import { useAppDispatch, useAppSelector } from '../../store/hooks';
    import { useEffect } from 'react';
    import {
      fetchUsers,
      fetchUserById,
      createUser,
      updateUser,
      deleteUser,
    } from './userThunks';
    import {
      selectUsers,
      selectSelectedUser,
      selectUserLoading,
      selectUserError,
    } from './userSelectors';
    
    export const useUsers = () => {
      const dispatch = useAppDispatch();
      const users = useAppSelector(selectUsers);
      const loading = useAppSelector(selectUserLoading);
      const error = useAppSelector(selectUserError);
    
      useEffect(() => {
        dispatch(fetchUsers());
      }, [dispatch]);
    
      return { users, loading, error };
    };
    
    export const useUser = (id: string) => {
      const dispatch = useAppDispatch();
      const user = useAppSelector(selectSelectedUser);
      const loading = useAppSelector(selectUserLoading);
      const error = useAppSelector(selectUserError);
    
      useEffect(() => {
        dispatch(fetchUserById(id));
      }, [dispatch, id]);
    
      return { user, loading, error };
    };
    
    export const useCreateUser = () => {
      const dispatch = useAppDispatch();
      return (user: User) => dispatch(createUser(user));
    };
    
    export const useUpdateUser = () => {
      const dispatch = useAppDispatch();
      return (user: User) => dispatch(updateUser(user));
    };
    
    export const useDeleteUser = () => {
      const dispatch = useAppDispatch();
      return (id: string) => dispatch(deleteUser(id));
    };
    

    e. Pages (pages/)

    UserList.tsx

    app/features/user/pages/UserList.tsx

    import React from 'react';
    import { useUsers, useDeleteUser } from '../userHooks';
    import { Link } from 'react-router-dom';
    
    const UserList: React.FC = () => {
      const { users, loading, error } = useUsers();
      const deleteUser = useDeleteUser();
    
      if (loading) return <p>Chargement...</p>;
      if (error) return <p>Erreur : {error}</p>;
    
      return (
        <div>
          <h1>Liste des utilisateurs</h1>
          <Link to="/users/new">Créer un nouvel utilisateur</Link>
          <ul>
            {users.map(user => (
              <li key={user.id}>
                <Link to={`/users/${user.id}`}>{user.name}</Link> ({user.email}) - {user.role}
                <button onClick={() => deleteUser(user.id)}>Supprimer</button>
              </li>
            ))}
          </ul>
        </div>
      );
    };
    
    export default UserList;
    
    UserDetail.tsx

    app/features/user/pages/UserDetail.tsx

    import React from 'react';
    import { useParams, Link } from 'react-router-dom';
    import { useUser } from '../userHooks';
    
    const UserDetail: React.FC = () => {
      const { id } = useParams<{ id: string }>();
      const { user, loading, error } = useUser(id!);
    
      if (loading) return <p>Chargement...</p>;
      if (error) return <p>Erreur : {error}</p>;
      if (!user) return <p>Utilisateur non trouvé</p>;
    
      return (
        <div>
          <h1>Détails de l'utilisateur</h1>
          <p>Nom : {user.name}</p>
          <p>Email : {user.email}</p>
          <p>Rôle : {user.role}</p>
          <Link to={`/users/${user.id}/edit`}>Modifier</Link>
        </div>
      );
    };
    
    export default UserDetail;
    
    UserForm.tsx

    app/features/user/pages/UserForm.tsx

    import React, { useEffect } from 'react';
    import { useForm } from 'react-hook-form';
    import { User } from '../../../domain/entities/User';
    import { useCreateUser, useUpdateUser, useUser } from '../userHooks';
    import { useNavigate, useParams } from 'react-router-dom';
    
    const UserForm: React.FC = () => {
      const { id } = useParams<{ id: string }>();
      const isEditMode = Boolean(id);
      const { register, handleSubmit, reset } = useForm<User>();
      const createUser = useCreateUser();
      const updateUser = useUpdateUser();
      const navigate = useNavigate();
      const { user } = useUser(id!);
    
      useEffect(() => {
        if (isEditMode && user) {
          reset(user);
        }
      }, [isEditMode, user, reset]);
    
      const onSubmit = async (data: User) => {
        if (isEditMode) {
          await updateUser({ ...data, id: id! });
        } else {
          await createUser(data);
        }
        navigate('/users');
      };
    
      return (
        <div>
          <h1>{isEditMode ? 'Modifier' : 'Créer'} un utilisateur</h1>
          <form onSubmit={handleSubmit(onSubmit)}>
            <div>
              <label>Nom</label>
              <input {...register('name', { required: true })} />
            </div>
            <div>
              <label>Email</label>
              <input {...register('email', { required: true })} />
            </div>
            <div>
              <label>Rôle</label>
              <input {...register('role', { required: true })} />
            </div>
            <button type="submit">{isEditMode ? 'Mettre à jour' : 'Créer'}</button>
          </form>
        </div>
      );
    };
    
    export default UserForm;
    

    3. Routes (routes/)

    app/routes/userRoutes.ts

    import React from 'react';
    import { Route, Routes } from 'react-router-dom';
    import UserList from '../features/user/pages/UserList';
    import UserDetail from '../features/user/pages/UserDetail';
    import UserForm from '../features/user/pages/UserForm';
    
    const UserRoutes: React.FC = () => (
      <Routes>
        <Route path="/users" element={<UserList />} />
        <Route path="/users/new" element={<UserForm />} />
        <Route path="/users/:id/edit" element={<UserForm />} />
        <Route path="/users/:id" element={<UserDetail />} />
      </Routes>
    );
    
    export default UserRoutes;
    

    4. Layout (layout/)

    app/layout/FullPage.tsx

    import React from 'react';
    
    const FullPageLayout: React.FC = ({ children }) => {
      return (
        <div className="full-page-layout">
          {children}
        </div>
      );
    };
    
    export default FullPageLayout;
    

    app/layout/Dashboard.tsx

    import React from 'react';
    import { Link, Outlet } from 'react-router-dom';
    
    const DashboardLayout: React.FC = () => {
      return (
        <div className="dashboard-layout">
          <nav>
            <ul>
              <li><Link to="/users">Utilisateurs</Link></li>
              {/* Autres liens de navigation */}
            </ul>
          </nav>
          <main>
            <Outlet />
          </main>
        </div>
      );
    };
    
    export default DashboardLayout;
    

    5. Point d’entrée principal

    app/App.tsx

    import React from 'react';
    import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
    import DashboardLayout from './layout/Dashboard';
    import FullPageLayout from './layout/FullPage';
    import UserRoutes from './routes/userRoutes';
    
    const App: React.FC = () => {
      return (
        <Router>
          <Routes>
            <Route path="/" element={<DashboardLayout />}>
              {/* Routes principales */}
              <Route path="users/*" element={<UserRoutes />} />
            </Route>
            {/* Routes avec un autre layout */}
            <Route path="/login" element={<FullPageLayout>{/* Composant de connexion */}</FullPageLayout>} />
          </Routes>
        </Router>
      );
    };
    
    export default App;
    

    Styles (styles/)

    Le dossier styles/ contient vos fichiers CSS ou SCSS pour styliser votre application.

    Utils (utils/)

    Le dossier utils/ contient des fonctions utilitaires réutilisables dans votre application.

    Conclusion

    En adoptant une architecture hexagonale dans votre application React avec Redux, vous bénéficiez d’une séparation claire des responsabilités :

    • Domaine : Contient la logique métier pure et est indépendant des frameworks et bibliothèques.
    • Infrastructure : Gère les détails techniques comme les appels API et le stockage.
    • Application : Gère l’interface utilisateur et l’état de l’application.

    En modifiant les thunks pour qu’ils utilisent les services du domaine plutôt que d’accéder directement à l’infrastructure, vous respectez pleinement les principes de l’architecture hexagonale. Cela permet de garder votre couche d’application indépendante des détails techniques de l’infrastructure.

    Cette architecture facilite la maintenance, les tests et l’évolutivité de votre application.

    Ressources supplémentaires

    Vous pouvez consulter ma proposition d’archi hexagonal en Golang