Luca Mele
Luca Mele

Architecture

Keep It Simple, Stupid : pourquoi le code le plus intelligent est le plus bête

Keep It Simple, Stupid : pourquoi le code le plus intelligent est le plus bête
Retour aux articles
·8 min de lecture

Il y a un pattern que je vois dans presque chaque équipe que je rejoins : un développeur découvre une abstraction puissante — Redux, MobX, XState, un event bus custom — et l'installe « parce qu'on en aura besoin un jour. » Six mois plus tard, l'équipe maintient 200 lignes de boilerplate pour un état qui pourrait vivre dans un appel useState. Personne ne se souvient pourquoi le store existe. Personne n'ose le supprimer.

C'est l'opposé du bon engineering. Le bon engineering, c'est choisir l'outil le plus simple qui résout le problème réel. KISS — Keep It Simple, Stupid — n'est pas un acronyme mignon. C'est le principe le plus sous-estimé du développement logiciel, particulièrement dans l'écosystème React où le paysage d'outils encourage activement le sur-engineering.

Le piège de la complexité

L'effet Dunning-Kruger frappe le plus fort dans la gestion d'état. Un développeur qui vient d'apprendre Redux pense que chaque morceau d'état appartient à un store global. Il crée des action types, des reducers, des selectors, du middleware — pour un filtre de recherche utilisé par deux composants sur une page. Ça semble productif. Ça semble professionnel. Ce n'est rien de tout ça.

Le code ingénieux est un passif. Chaque abstraction que vous introduisez est un concept que votre équipe doit apprendre, maintenir et déboguer. Redux ajoute des actions, des reducers, des selectors, du middleware et une couche de configuration du store. Ce sont cinq nouveaux concepts pour la gestion d'état — dans un framework qui a déjà une gestion d'état intégrée. Vous payez une taxe de complexité sur chaque fonctionnalité, et le retour sur cet investissement est rarement positif.

J'ai rejoint des projets chez AXA et dans une grande banque suisse où la suppression complète de Redux — remplacé par useState et useContext — a réduit le code de gestion d'état de 70% et accéléré le développement de fonctionnalités. Pas parce que Redux est un mauvais logiciel. Parce que c'était le mauvais outil pour ce dont ces applications avaient réellement besoin.

// Redux approach for a simple filter + list UI
// store/filtersSlice.ts
const filtersSlice = createSlice({
  name: 'filters',
  initialState: { search: '', category: 'all' },
  reducers: {
    setSearch: (state, action) => { state.search = action.payload; },
    setCategory: (state, action) => { state.category = action.payload; },
    resetFilters: (state) => { state.search = ''; state.category = 'all'; },
  },
});
// store/index.ts — configure store, combine reducers
// store/hooks.ts — typed useSelector, useDispatch
// components/Filters.tsx — useSelector + useDispatch
// components/List.tsx — useSelector to read filters
// 5 files, 80+ lines of boilerplate for two strings.

// The same thing with useState:
function ProductPage() {
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('all');

  return (
    <>
      <Filters search={search} category={category}
        onSearch={setSearch} onCategory={setCategory} />
      <ProductList search={search} category={category} />
    </>
  );
}
// 1 file. 10 lines. Same result. Easier to read, easier to change.

React State suffit (la plupart du temps)

React a deux primitives d'état intégrées : useState pour l'état local, et useContext pour l'état partagé. Ensemble, elles couvrent la grande majorité des applications réelles. Avant de chercher une librairie externe, demandez-vous : puis-je résoudre ça avec ce que React me donne déjà ?

useState gère l'état local au composant — les champs de formulaire, les toggles, les flags de chargement, les onglets sélectionnés. useContext gère l'état dont plusieurs composants ont besoin — l'utilisateur courant, un panier, une préférence de thème. Combinez-les et vous avez une solution de gestion d'état sans dépendance, sans boilerplate, et comprise par chaque développeur React sur la planète.

L'objection que j'entends concerne toujours la « scalabilité ». Mais j'ai travaillé sur de grandes applications chez Vontobel et Migros — des plateformes de trading en temps réel, du e-commerce multi-marques — et Context + useState ont bien scalé. La clé est de placer les providers stratégiquement : pas un context géant à la racine, mais des contexts focalisés pour chaque domaine. Un CartProvider entoure le checkout. Un ThemeProvider entoure l'app. Un DashboardProvider entoure la page dashboard. Chacun est petit, focalisé et facile à comprendre.

// When multiple components need the same data and
// props drilling goes beyond 3 levels — use Context.

interface CartContext {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  total: number;
}

const CartCtx = createContext<CartContext | null>(null);

function useCart() {
  const ctx = useContext(CartCtx);
  if (!ctx) throw new Error('useCart must be used within CartProvider');
  return ctx;
}

// Smart component: owns the state, provides it via context
function CartProvider({ children }: { children: ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);

  const addItem = (item: CartItem) =>
    setItems((prev) => [...prev, item]);

  const removeItem = (id: string) =>
    setItems((prev) => prev.filter((i) => i.id !== id));

  const total = items.reduce((sum, i) => sum + i.price, 0);

  return (
    <CartCtx.Provider value={{ items, addItem, removeItem, total }}>
      {children}
    </CartCtx.Provider>
  );
}

// Any descendant can read cart state — no prop chains
function CartBadge() {
  const { items } = useCart();
  return <span>{items.length}</span>;
}

function CartTotal() {
  const { total } = useCart();
  return <span>{formatCurrency(total)}</span>;
}

L'arbre de composants est votre gestionnaire d'état

Cela rejoint directement la séparation smart et dumb components dont j'ai déjà parlé. L'arbre de composants n'est pas qu'une hiérarchie de rendu — c'est un système de distribution d'état. Les smart components en haut possèdent l'état et le transmettent vers le bas. Les dumb components en dessous reçoivent les données et les affichent.

Quand vous placez l'état au bon endroit — l'ancêtre commun le plus bas des composants qui en ont besoin — vous n'avez pas besoin de store. L'arbre React fait la distribution gratuitement. Un composant DashboardPage qui récupère les données et les passe à StatsGrid et ChartSection fait exactement ce que Redux ferait, mais sans cérémonie.

C'est ce que je veux dire par « le code le plus bête est le plus intelligent ». Un smart component avec useState et trois props passées aux enfants, c'est ennuyeux. C'est évident. Un nouveau développeur peut le lire en 30 secondes. C'est exactement ce que vous voulez. La réécriture du panier Next.js que j'ai décrite dans Oubliez les best practices suivait le même principe — nous avons remplacé la complexité du rendu serveur par un état côté client simple, et tout s'est amélioré.

// The component tree IS your state architecture.
// Place state in the lowest common ancestor that needs it.

// ❌ Over-engineered: global store for page-local state
// Redux store → useSelector in Header, Sidebar, Content
// Now every component is coupled to a global store shape
// and re-renders on any store change unless you memoize everything.

// ✅ Simple: smart component owns the state, passes it down
function DashboardPage() {
  const [period, setPeriod] = useState<'week' | 'month' | 'year'>('month');
  const [data, setData] = useState<DashboardData | null>(null);

  useEffect(() => {
    fetchDashboard(period).then(setData);
  }, [period]);

  return (
    <main>
      {/* 1 level deep — just pass it */}
      <PeriodSelector period={period} onChange={setPeriod} />
      <StatsGrid data={data} />
      <ChartSection data={data} period={period} />
    </main>
  );
}

// PeriodSelector, StatsGrid, ChartSection are all dumb.
// They receive data, render UI, report interactions.
// No store subscription. No selector. No dispatch.
// Change the data shape? Update one smart component.

Props, Context, ou les deux

J'impose une règle simple dans mes équipes : le props drilling est acceptable jusqu'à 3 niveaux de profondeur. Au-delà, utilisez Context. Ce n'est pas arbitraire — c'est le point où passer la même prop à travers des composants intermédiaires qui ne l'utilisent pas commence à nuire à la lisibilité et crée un couplage inutile.

Les props sont le défaut. Elles sont explicites, traçables et type-safe. Quand vous voyez les props d'un composant, vous savez exactement ce dont il a besoin. Cette transparence est précieuse — ne la sacrifiez pas pour la commodité.

Context est pour quand les props créent des chaînes. Quand un composant Layout doit accepter une prop user juste pour la passer à un composant Navigation qui la passe à un UserMenu — c'est du bruit. Aucun de ces composants intermédiaires ne se soucie de l'utilisateur. Context vous permet de sauter la chaîne et de lire la valeur directement là où elle est nécessaire.

Le juste milieu est de mixer les deux. Utilisez Context pour les préoccupations transversales dont beaucoup de composants ont besoin (utilisateur, thème, panier, locale). Utilisez les props pour les données qui coulent à travers une relation parent-enfant claire. Et n'utilisez ni l'un ni l'autre pour l'état qui appartient à un seul composant — c'est à ça que sert useState.

// Props drilling is fine — up to a point.
// My rule: max 3 levels deep. Beyond that, use Context.

// ✅ Fine — 2 levels
<Page>
  <Section filters={filters}>
    <FilterBar filters={filters} onChange={setFilters} />
  </Section>
</Page>

// ❌ Too deep — 5 levels of threading the same prop
<Page>
  <Layout user={user}>
    <Sidebar user={user}>
      <Navigation user={user}>
        <UserMenu user={user}>      {/* enough. */}
          <Avatar user={user} />
        </UserMenu>
      </Navigation>
    </Sidebar>
  </Layout>
</Page>

// ✅ Context cuts the chain where it matters
<UserProvider user={user}>
  <Layout>
    <Sidebar>
      <Navigation>
        <UserMenu />    {/* useUser() — reads from context */}
      </Navigation>
    </Sidebar>
  </Layout>
</UserProvider>

// The middle components (Layout, Sidebar, Navigation)
// no longer need to know about 'user' at all.
// They just render their children. Clean.

Redondance plutôt qu'abstraction

Cet article est le compagnon pratique de Pourquoi YAGNI bat DRY. Le même principe s'applique à la gestion d'état : un peu de redondance est moins cher que la mauvaise abstraction. Deux composants qui gèrent chacun leur propre état local avec des appels useState similaires sont plus faciles à maintenir que deux composants partageant une tranche de store global conçue pour « éviter la duplication ».

La duplication dans l'état est visible et locale. Vous pouvez la voir, et si les deux états doivent diverger (c'est souvent le cas), vous changez simplement l'un sans toucher l'autre. Un store partagé crée du couplage invisible — changez la forme pour un consommateur et vous cassez l'autre. J'ai vu des équipes passer des sprints entiers à démêler des tranches Redux partagées qui « économisaient du code » mais créaient des dépendances entre des fonctionnalités sans rapport.

Les meilleures équipes avec lesquelles j'ai travaillé choisissent le simple par défaut. useState d'abord. Si plusieurs composants ont besoin du même état, remontez-le vers un parent commun et transmettez-le. Si la chaîne de props devient trop profonde, enveloppez-le dans un Context. Si — et seulement si — vous avez genuinement besoin de time-travel debugging, de machines d'état complexes ou de mises à jour optimistes sur de nombreuses entités, alors envisagez une librairie dédiée. Mais la plupart des applications n'en arrivent jamais là. Et celles qui y arrivent en ont habituellement besoin dans une partie de l'app, pas partout.

Arrêtez de résoudre des problèmes que vous n'avez pas. La solution la plus simple qui fonctionne est la meilleure solution — et elle reste presque toujours la meilleure solution bien après que la solution ingénieuse est devenue un cauchemar de maintenance.