Respecter les principes SOLID avec React

Les principes SOLID sont un ensemble de cinq concepts fondamentaux en programmation. Ils sont majoritairement appliqués en POO (Programmation Orienté Objet). On peux également les appliquer ailleurs. Ils sont indispensables en architecture logiciel. Pour vous présenter ces principes, j'ai fait le choix d'utiliser Javascript plutôt que Typescript pour ne pas alourdir les exemples. Nous allons appliquer ces principes à travers une simple "Todo List".
•
Mis à jour le 14 août 2026
•
15 min de lecture
Believemy logo

Qu'est ce que les principes SOLID ?

L'acronyme S.O.L.I.D a été inventé par Michael Feathers à partir des principes de programmation orientée objet identifiés par Robert Cecil Martin AKA Uncle Bob ( Clean Code Handbook / Clean Coder )

Ces principes visent à rendre le code plus lisible (propre), facile à maintenir, facile à faire évoluer, réutilisable et sans duplication de code.

Par « facile », il faut comprendre que cela signifie que le coût nécessaire pour effectuer un changement à l’application devrait toujours être inférieur aux bénéfices apportés directement par ce changement.

En tant que développeurs, en suivant ces principes,  nous pouvons concevoir des applications robuste, flexible, extensibles et plus faciles à maintenir. 

Appliquer ces principes contribuent nettement à avoir un code de qualité et à assurer une meilleure gestion du cycle de vie de nos applications.

Note :
Comprendre les 5 principes SOLID et de les utiliser, nous permettra : 

  • D’améliorer la qualité du code.
  • De comprendre des codes plus évolués.
  • De diminuer la dette technique de nos projets. 

 

Utiliser ces principes doit devenir une habitude dans votre quotidien.

 

Le S pour SRP : Single Responsibility Principle (Le Principe de Responsabilité Unique)

Le principe de responsabilité unique stipule qu'une classe ne doit avoir qu'une seule raison de changer, c'est-à-dire qu'une classe (composant, fonction) devrait avoir une seule responsabilité.

Cela nous permet de nous assurer qu’une fonction ne fait qu’une seule et unique chose, mais qu’elle le fait bien._ (cf. SOC : Separation Of Concern).

Mauvais exemple :
Le composant gère à la fois l'affichage et la logique de gestion des tâches.

JSX
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);

  const fetchTodos = () => {
    fetch('https://api.example.com/todos')
      .then(response => response.json())
      .then(data => setTodos(data))
      .catch(error => console.error(error));
  };

  useEffect(() => {
    fetchTodos(); // Récupère les tâches depuis une API
  }, []);

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          {todo.title}
        </div>
      ))}
    </div>
  );
};

 

Bon exemple :
Diviser le composant en deux : 

  • un pour l'affichage
  • un pour la récupération des tâches.
JSX
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);

  const fetchTodos = () => {
    fetch('https://api.example.com/todos')
      .then(response => response.json())
      .then(data => setTodos(data))
      .catch(error => console.error(error));
  };

  useEffect(() => {
    fetchTodos(); // Récupère les tâches depuis une API
  }, []);

  return (
    <TodoListItems todos={todos} />
  );
};

const TodoListItems = ({ todos }) => {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.title}
        </li>
      ))}
    </ul>
  );
};


Dans le mauvais exemple, le composant TodoListWidget gère à la fois la récupération des tâches depuis une API et l'affichage des tâches.

Cela viole le principe de responsabilité unique car un composant devrait faire qu'une seule et unique chose.

Dans le bon exemple, nous avons divisé le composant TodoListWidget en deux composants distincts : TodoListWidget et TodoListItems.

Le composant TodoListWidget est responsable de la récupération des tâches depuis l'API et il rend le composant TodoListItems qui est responsable de l'affichage des tâches.

Le code est plus clair, maintenable et respecte ce principe, chaque composant a une seule responsabilité.

On peux aller encore plus loin, car le composant  TodoListItems lui aussi viole le principe de responsabilité unique car il s'occupe également d'afficher un  item.

Celui-ci pourrait donc faire l'objet d'un composant supplémentaire TodoItem.

Question :
Pourquoi utiliser le principe de responsabilité unique ?

  • Permet de découpler fortement les composants des applications.
  • Evite les classes à rallonge, le code est beaucoup plus clair.
  • Le code est plus maintenable, plus facile et agréable à faire évoluer. 
  • Chaque composant, classe, chaque fichier à un rôle qui lui est propre.

 

Le O pour OCP : Open/Closed Principle (Principe d'Ouverture/Fermeture)

Le principe d'Ouverture/Fermeture préconise que les entités logicielles, telles que les classes et les modules, devraient être ouvertes pour extension, mais fermées pour modification.

Une classe ou un module devrait pouvoir être étendu pour ajouter de nouvelles fonctionnalités sans modifier son code source existant.

Cela facilite l'ajout de nouvelles méthodes sans perturber le comportement existant.

Plus simplement

  • Cela signifie que l'on doit favoriser l'extension du code plutôt que sa modification. 
  • Cela nous demande de ne pas modifier le comportement d’une action en fonction d’un paramètre, mais plutôt d’étendre les capacités de ce paramètre grâce à une fonction définie en amont.

 

Mauvais exemple :
Le composant TodoListItems effectue directement l'ajout d'une nouvelle tâche.

JSX
const TodoListItems = () => {
  const [todos, setTodos] = useState([]);

  /* ... */

  const addTodo = (newTodo) => {
    setTodos([...todos, newTodo]);
  };

  return (
        <>
        <div>
            <label htmlFor="new-todo-item">Add Todo</label>
            <div>
                <input type="text" placeholder="New Todo" value={newTodoTitle}
                    onChange={handleTitleChange}/>
                <button
                    onClick={addTodo}>Add
                </button>
            </div>
       </div>
       <ul>
            {todos.map(todo => (
                <li key={todo.id}>
                    {todo.title}
                </li>
            ))}
       </ul>
    </>
  );
};

 

Bon exemple :
Utiliser un gestionnaire externe pour l'ajout de nouvelles tâches.

JSX
const TodoListItems = ({ todos, addTodoHandler }) => {

 /* logique necessaire au bon fonctionnement */

  return (
     <>
        <div>
            <label htmlFor="new-todo-item">Add Todo</label>
            <div>
                <input type="text" placeholder="New Todo" value={newTodoTitle}
                    onChange={handleTitleChange}/>
                <button
                    onClick={addTodoHandler}>Add
                </button>
            </div>
       </div>
       <ul>
            {todos.map(todo => (
                <li key={todo.id}>
                    {todo.title}
                </li>
            ))}
       </ul>
    </>
  );
};

// Gestionnaire externe pour les tâches
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);

  const handleAddTodo = (newTodo) => {
    setTodos([...todos, newTodo]);
  };

  return (
    <>
      <TodoListItems todos={todos} addTodoHandler={handleAddTodo} />
    </>  
  );
};


Dans le mauvais exemple, le composant TodoListItems effectue directement l'ajout d'une nouvelle tâche en utilisant la fonction addTodo.

Cela viole le principe d'Ouverture/Fermeture car le code est ouvert à la modification directe pour ajouter de nouvelles fonctionnalités, ce qui peut entraîner des effets de bord indésirables.

Dans le bon exemple, nous utilisons un gestionnaire externe TodoListWidget pour gérer les tâches. Ce gestionnaire contient la logique pour ajouter de nouvelles tâches (addTodoHandler). Le composant TodoListItems reçoit les tâches et la fonction addTodoHandle en tant que props, sans se soucier de la façon dont la gestion des tâches est effectuée.

Le composant TodoListItems est fermé pour les modifications directes et ouvert à l'extension pour ajouter de nouvelles fonctionnalités. En utilisant le gestionnaire externe, on respecte le principe d' Ouverture/Fermeture.

Notez bien que dans ce bon exemple, le composant TodoListItems viole également le 1er principe de responsabilité unique comme vu précédemment. Ce n'est donc pas 100% correcte.

 

Le L pour LSP : Liskov Substitution Principle (Principe de Substitution de Liskov)

Le principe de Substitution de Liskov stipule qu'un objet d'une classe dérivée doit pouvoir être substitué (remplaçable) à un objet de la classe de base sans affecter la cohérence du programme. 

En d'autres termes, les classes  enfants ne peuvent pas faire plus ou moins que leur parent. On peut ainsi interchanger les enfants sans que cela ait d’incidence sur l’exécution du code.

La hiérarchie d'héritage est solide et évite les surprises ou les erreurs lors de l'utilisation de sous-classes.

Pour être conforme a ce principe, il y a quelques grandes conditions à respecter :

  • La signature des fonctions (paramètres et retour) doit être identique entre l’enfant et le parent.
  • Les paramètres de la fonction de l’enfant ne peuvent pas être plus nombreux que ceux du parent.
  • Le retour de la fonction doit retourner le même type que le parent.
  • Les exceptions/erreurs retournées doivent être les mêmes.

 

Mauvais exemple :
Le gestionnaire de tâches ne prend en charge que les tâches de type chaîne de caractères.

JSX
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);

  const addTodo = (newTodo) => {
    if (typeof newTodo === 'string') {
      setTodos([...todos, newTodo]);
    } else {
      throw new Error('Invalid todo type');
    }
  };

  return (
    <TodoListItems todos={todos} addTodo={addTodo} />
  );
};

 

Bon exemple :
Le gestionnaire de tâches accepte tous les types de tâches valides.

JSX
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);

  const addTodo = (newTodo) => {
    setTodos([...todos, newTodo]);
  };

  return (
    <TodoListItems todos={todos} addTodo={addTodo} />
  );
};

Dans le mauvais exemple, le gestionnaire de tâches TodoListWidget ne prend en charge que les tâches de type chaîne de caractères. Si un autre type de donnée est passé, il lance une erreur.

Cela viole le principe de Substitution de Liskov car le gestionnaire ne peut pas être substitué par un autre qui accepte d'autres types de tâches.

Dans le bon exemple, le gestionnaire de tâches TodoListWidget accepte n'importe quel type de tâche. Il ne fait pas de vérification de type avant d'ajouter la nouvelle tâche à la liste.

Cela respecte le principe de Substitution de Liskov car le gestionnaire peut être substitué par un autre qui accepte des types de tâches similaires sans changer son comportement global.

Question :
Pourquoi utiliser le principe  de Substitution de Liskov ?

  • Evite les bugs où un enfant fait (surtout) plus qu’un parent.
  • Facilite l'organisation lors de mises à jour. Chaque changement du parent induit
         des changements vers les enfants, ce qui est plus sécurisé.
  • On gagne en lisibilité et qualité, le code est beaucoup plus simple à lire et à (ré-)utiliser.

 

Le I pour ISP : Interface segragation Principle (Principe d'Interface de Ségrégation)

Le principe d'interface de ségrégation recommande de diviser les interfaces en fonctions spécifiques aux besoins. 

Cela signifie que nous ne devons pas implémenter des méthodes ou utiliser des informations dont on n'a pas besoin. À la place, on passe un contrat avec l’entité qui en a besoin.

En séparant les interfaces (composants, fonctions ...) en de plus petites interfaces spécifiques, on évite le fardeau des méthodes inutiles et on rend l'interface plus cohérente avec les besoins spécifiques.

Mauvais exemple :
Tous les details de la tâche todo sont passés au composant TodoItemCheckbox

JSX
const TodoItemCheckbox = ({todo}) => {
  return (
    <>
      <input type="checkbox" {todo.isCompleted ? "checked" : ""}>
    </>
  )
}

const TodoItem = ({todo}) => {
  return (
    <li>
      <TodoItemCheckbox todo={todo}/>
      <span>todo.title</span>
     </li>
  )
}

const TodoListItems = ({ todos }) => {
  return (
    <ul>
      {todos.map(todo => (
        <TodoListItem key={todo.id} todo={todo}/>                  
      ))}
    </ul>
  );
};

 

Bon exemple :
Seule l'information nécessaire est passée au composant TodoItemCheckbox

JSX
const TodoItemCheckbox = ({isChecked}) => {
  return (
    <>
      <input type="checkbox" {isChecked ? "checked" : ""}>
    </>
  )
}

const TodoItem = ({todo}) => {
  return (
    <li>
      <TodoItemCheckbox isChecked={todo.isCompleted}/>
      <span>todo.title</span>
     </li>
  )
}

const TodoListItems = ({ todos }) => {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo}/>                  
      ))}
    </ul>
  );
};

Dans le mauvais code, tous les details de la tâche sont passés au composant TodoItemCheckbox alors que cela n'est pas nécessaire. On ajoute des risques d'erreurs et une complexité inutiles au composant. Cela peut être problématique car le composant se retrouve avec des fonctionnalités et/ou des informations inutiles.

Il viole le principe de "Séparation" des Interfaces qui recommande de diviser les "interfaces" en parties distinctes pour chaque fonctionnalité.

Dans le bon code, le composant TodoItemCheckbox reçois uniquement l'information requise isChecked qui correspond à son besoin au lieu de tous les détails de la tâche.

On évite ainsi d'avoir des fonctionnalités/informations inutiles . Cela respecte le principe de Ségrégation des interfaces.

Question :
Pourquoi utiliser le principe de ségrégation d'interface ?

  • La qualité du code est améliorée.
  • Le code est plus modulable et maintenable
  • Le code est plus facile à lire et à comprendre
  • On respecte les principes de  responsabilité unique et d' Ouverture/Fermeture

 

Le D pour DIP : Dependency Inversion Principle (Principe d'Inversion de Dépendance)

Pour ce dernier on le traduit également par dependency injection,  injection de dépendance en français.

Le principe d'inversion de dépendance, est surement l'un des plus important avec le principe de responsabilité unique. La règle veut que les classes, modules de haut niveau  doivent dépendre de leurs abstractions et non pas de leurs implémentations.

Les abstractions ne doivent pas dépendre des détails, ce sont les détails qui doivent dépendre des abstractions.

Plus simplement.

On évite de passer des objets en paramètre lorsqu’une interface est disponible. On par ce fait, certain que l’objet que l'on manipule, peu importe son type, celui-ci aura les bonnes méthodes/types associés.

Cela nous demande de passer des abstractions en paramètre. Des contrats notamment grâce aux interfaces (pour les languages Orienté Objet),  plutôt que les objets eux-mêmes

Mauvais exemple :
Le composant TodoListWidget dépend directement de la fonction fetchTodos.

JSX
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);
  
  const fetchTodos = () => { 
    fetch('https://api.example.com/todos') 
    .then(response => response.json()) 
    .then(data => setTodos(data)) 
    .catch(error => console.error(error)); 
  };
  
  useEffect(() => {
    fetchTodos(); // Récupère les tâches depuis une API
  }, []);

  // Utilisation des tâches récupérées
};

 

Bon exemple :
Utilisation d'un gestionnaire (fonction) externe pour inverser la dépendance
entre TodoListWidget et la récupération des tâches.

JSX
const TodoListWidget = ({fetchCallback}) => {
  const [todos, setTodos] = useState([]);
  
  const fetchTodos = () => {
     // Code pour récupérer les tâches depuis une API ou autre
    const todoItems = fetchCallback(); // appel a notre fonction callback injectée
    setTodos(todoItems);
  };
  
  useEffect(() => {
    fetchTodos()  
  }, []);

  // Autres traitements
};

Dans le mauvais exemple, le composant TodoListWidget dépend directement de la fonction fetchTodos pour récupérer les tâches depuis une API. Le composant est étroitement couplé à la logique de récupération des tâches. Ce qui rend difficile le remplacement de cette logique et pour effectuer des tests unitaires.

Dans le bon exemple, nous utilisons une fonction de rappel (callback) dans fetchTodos pour inverser la dépendance entre TodoListWidget et la récupération des tâches.

Le composant TodoListWidget ne se soucie plus de la façon dont les tâches sont récupérées, mais se contente de fournir une fonction de rappel qui sera appelée lors de la récupération des données initiales. 

Il est plus flexible et est indépendant de la source de données. On facilite, la mise en place de tests et nous permettra de remplacer facilement la logique de récupération des données si nécessaire. Cela respecte le principe d'Inversion de Dépendance.

Question :

Pourquoi utiliser le principe d' inversion de dépendance ?

  • Le code est plus flexible et facile à modifier, on peut rajouter des fonctionnalités sans crainte de bugs.
  • Le code est facilement réutilisable
  • L'écriture dees tests unitaires est facilité, en isolant les dépendances.
  • Augmente de la qualité du code et de sa lisibilité

 

Oups ! y'a un hic dans les exemples !

Certain d'entre vous l'on remarqué, dans les "bon exemples" les composants TodoListWidget et TodoListItems (ce n'est pas une erreur, c'est voulu) ne respectent pas les principes de Substitution de Liskov,  d' Open/Closed et de  Dependency Inversion.

En effet dans le composant TodoListItems on ne peux pas remplacer le composant TodoItem. Et dans TodoListWidget on ne peux pas remplacer le composant TodoListItems.

Alors comment faire avec React pour que notre composant TodoListWidget soit extensible et personnalisable ?

C'est assez simple en fait.  Nous allons nous servir majoritairement du principe d'inversion de dépendance pour pouvoir interchanger nos composants TodoListItems et TodoItem pour  pouvoir respecter les principes de substitution de Liskov et  d'ouverture/fermeture. 

Pour pouvoir réaliser ce petit  tour de passe-passe,  nous allons, à notre composant TodoWidget, ajouter en paramètre des "providers".  Ces "providers" sont sous forme de fonction. Ils seront chargés de nous renvoyer le rendu de nos "sous-composants" personnalisés.

Voici comment le faire pour un Item de la liste des tâches.

JSX
export const TodoListWidget = ({ fetchCallback, customListItemProvider }) => {
  const [todos, setTodos] = useState([])

  const fetchTodos = () => {
    // Code pour récupérer les tâches depuis une API ou autre
    const todoItems = fetchCallback();
    setTodos(todoItems);
  };

  const handleAddTodo = (newTodo) => {
    setTodos([...todos, newTodo])
  }
  const handleDeleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  const handleToggleCompleted = (id) => {
    setTodos(todos.map(todo => {
      if (todo.id === id) {
        return {...todo, completed: !todo.completed}
      }
      return todo
    }))
  }

  useEffect(() => {
    fetchTodos()
  }, []);

  return (
    <div>
      <h1>Todo List Widget</h1>
      <hr/>      
      <TodoListItems todos={todos} 
                     deleteTodoHandler={handleDeleteTodo} 
                     toggleTodoCompletedHandler={handleToggleCompleted} 
                     customListItemProvider={customListItemProvider}/>
    </div>
  )
}

export const TodoListItems = ({todos, deleteTodoHandler, toggleTodoCompletedHandler, customListItemProvider}) => {
  return (
    <div>
      {todos.length === 0 && <p className={'text-center'}>Aucune tâche à afficher</p>}
      {todos.length > 0 &&
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>
              {customListItemProvider(todo, deleteTodoHandler, toggleTodoCompletedHandler)}
            </li>
          ))}
        </ul>
      }
    </div>
  )
}

export const TodoItem = ({item, deleteTodoHandler, toggleTodoCompletedHandler}) => {
  return (
      <div>
        <input id={`check-${item.id}`} type="checkbox" defaultChecked={item.completed}      
               onClick={() => toggleTodoCompletedHandler(item.id)}/>
        <label htmlFor={`check-${item.id}`}>{item.title}</label>
        <button onClick={() => deleteTodoHandler(item.id)}>X</button>
      </div>
  )
}

Et dans notre App.jsx

JSX
import {TodoListWidget} from "./components/Todolist/TodoListWidget.jsx";
import {getData} from "./data/todoListData.js";
import {getData as getDataExt} from "./data/todoListDataExt.js";
import {TodoItemExt} from "./components/Todolist/TodoListExtended/TodoItemExt.jsx";
import {TodoItem} from "./components/Todolist/TodoItem.jsx";

function App() {

  const todoItemProvider = (todo, deleteTodoHandler, toggleTodoCompletedHandler) => {
    return <TodoItem item={todo} 
                     deleteTodoHandler={deleteTodoHandler} 
                     toggleTodoCompletedHandler={toggleTodoCompletedHandler}/>
  }

  const todoItemExtProvider = (todo, deleteTodoHandler, toggleTodoCompletedHandler) => {
    return <TodoItemExt item={todo} 
                        deleteTodoHandler={deleteTodoHandler} 
                        toggleTodoCompletedHandler={toggleTodoCompletedHandler}/>
  }


  return (
    <div>
      <div">
        <TodoListWidget fetchCallback={getData} customListItemProvider={todoItemProvider}/>
        <TodoListWidget fetchCallback={getDataExt} customListItemProvider={todoItemExtProvider}/>
      </div>
    </div>
  )
}

export default App

Vous l'avez peut-être remarqué dans le code ci-dessus,  j'ai supprimé la props addTodoHandler du composant TodoListItems pourquoi ? Comme mentionné précédemment, le principe de Responsabilité Unique était enfreint. Ce n'est pas le role du composant TodoListItems d'ajouter une tâche dans nos données. Son unique rôle c'est d'afficher la liste des tâches.

Nous devons donc créer un nouveau composant TodoAddForm et utiliser celui-ci dans le composant TodoListWidget pour continuer à respecter les principes SOLID.

Je vous laisse faire ce petit changement par vous même.

 

Le code complet

Retouvez un exemple complet de ce  TodoListWidget respectant les principes SOLID sur mon Github : react-solid-todolist

 

Pour aller plus loin

Si vous voulez creuser encore plus ce domaine, allez donc jeter un oeil à notre formation React JS !

 

Réferences

Catégorie : Développeurs

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.

Believemy logo
Commentaires (0)

Vous devez être connecté pour commenter cet article : se connecter ou s'inscrire.

À lire ensuite

Utiliser le localStorage dans une application NextJs ou React

Cet article a pour but de vous apprendre à utiliser le localStorage dans une application Next.JS ou React. En effet, il y a quelques différences avec l'utilisation normale, dans une application vanilla.

21 août 2024

Code plus vite avec React grâce à ces techniques

Apprenez à coder plus vite avec React grâce à des techniques simples mais efficaces qu'utilisent les développeurs expérimentés.

20 septembre 2024

Composant par classe VS Composant par fonction avec React

Vous vous êtes toujours demandé la différence entre un composant utilisant une classe et un composant sous forme de fonction avec React ? Cet article récapitulatif est fait pour vous !

7 mai 2021

Pourquoi faut-il apprendre React en 2025 : tous les avantages !

Explorez pourquoi React reste un choix incontournable en 2025 pour les développeurs. Découvrez ses avantages, l'évolution de ses fonctionnalités et pourquoi le marché continue de le préférer.

11 octobre 2024

React vs Angular : différences, avantages et inconvénients en 2025

Découvrez qui de React ou Angular serait le meilleur choix pour votre projet en comprenant leurs avantages et leurs inconvénients dans notre comparatif complet.

9 septembre 2024

Quelles différences entre les "props" et le "state" avec React ?

Props... Propriétés... State... État... Au début, c'est toujours un peu... étrange ! Pour éviter d'avoir le moindre doute, faisons un petit récapitulatif des différences entrre les props et le state avec React.

7 mai 2021

CSS Flexbox - Apprendre les bases 2/2

Alors ça y est ! Vous êtes convaincu de l'utilité de flexbox ? Si oui, alors revenez, car je n'en ai pas fini avec vous 🙂 ! Cette fois-ci, nous attaquons les propriétés flex-direction et flex-wrap.

11 juillet 2022

5 méthodes à absolument connaître avec JavaScript ES 2023

Découvrez les 5 méthodes incontournables de JavaScript ES 2023. Boostez vos compétences avec les dernières nouveautés de l'ECMAScript avec les méthodes findLast(), findLastIndex(), et bien plus encore.

19 septembre 2024

Devenez le solopreneur que vous rêvez d'être.

Premier projet, reconversion, lancement de business : accédez à nos formations, pratiquez sur des cas concrets et avancez avec un accompagnement clair.