Retour au blog

Signals en JavaScript 2026 : Le Futur de la Réactivité Web

Salut HaWkers, l'une des propositions TC39 les plus attendues avance en 2026 : les Signals natifs en JavaScript. Cette feature promet d'unifier la façon dont les frameworks gèrent la réactivité et pourrait changer fondamentalement la manière dont nous construisons des applications web.

Explorons ce que sont les Signals, comment ils fonctionnent et leur impact sur l'écosystème.

Qu'est-ce que les Signals

Concept Fondamental

// Signal est un conteneur réactif pour les valeurs

// Analogie simple
const traditionalVariable = 5; // Valeur statique
// Si elle change, personne ne le sait

// Signal
const count = signal(5); // Valeur réactive
// Quand elle change, les dépendants sont notifiés automatiquement

Comment Ils Fonctionnent

// Implémentation conceptuelle (pas l'API finale du TC39)

// 1. Créer un signal
const count = signal(0);

// 2. Lire la valeur
console.log(count.value); // 0

// 3. Écrire une valeur
count.value = 1; // Notifie les dépendants

// 4. Computed (dérivé d'autres signals)
const doubled = computed(() => count.value * 2);
// doubled.value est toujours count.value * 2
// Recalcule automatiquement quand count change

// 5. Effect (side effect réactif)
effect(() => {
  console.log(`Count est maintenant : ${count.value}`);
});
// S'exécute chaque fois que count change

Pourquoi les Signals Sont Importants

Le Problème Actuel

// React : re-render du composant entier

function Counter() {
  const [count, setCount] = useState(0);

  // Quand count change :
  // 1. La fonction Counter s'exécute à nouveau
  // 2. Le Virtual DOM est créé
  // 3. Diff avec le DOM précédent
  // 4. Les patches sont appliqués

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {count * 2}</p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment
      </button>
    </div>
  );
}

// Même si seul count a changé, tout se re-rend
// Signals : mise à jour granulaire

function Counter() {
  const count = signal(0);
  const doubled = computed(() => count.value * 2);

  // Quand count change :
  // 1. Seul le texte utilisant count.value se met à jour
  // 2. Le computed doubled recalcule
  // 3. Seul le texte utilisant doubled se met à jour
  // 4. Aucun re-render de composant

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => count.value++}>
        Increment
      </button>
    </div>
  );
}

// Mise à jour chirurgicale, sans overhead

Comparaison de Performance

// Benchmark conceptuel

const performanceComparison = {
  react: {
    update: 'Re-render de composant',
    complexity: 'O(taille de l\'arbre)',
    overhead: 'Diff du Virtual DOM',
    ideal: 'UIs avec beaucoup de changements coordonnés'
  },

  signals: {
    update: 'Mutation directe du DOM',
    complexity: 'O(1) par signal',
    overhead: 'Tracking des dépendances',
    ideal: 'Updates fréquentes et granulaires'
  },

  realWorld: {
    smallApp: 'Différence négligeable',
    largeApp: 'Signals 2-10x plus rapide',
    animations: 'Signals bien supérieur',
    forms: 'Signals bien supérieur'
  }
};

Frameworks Qui Utilisent Déjà les Signals

SolidJS

// SolidJS : Signals depuis le début

import { createSignal, createEffect, createMemo } from 'solid-js';

function Counter() {
  // Signal primitif
  const [count, setCount] = createSignal(0);

  // Computed (memo)
  const doubled = createMemo(() => count() * 2);

  // Effect
  createEffect(() => {
    console.log('Count a changé:', count());
  });

  return (
    <div>
      <p>Count: {count()}</p>
      <p>Doubled: {doubled()}</p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment
      </button>
    </div>
  );
}

// Note : count() est une fonction, pas .value
// SolidJS utilise des fonctions pour le tracking

Vue 3

// Vue 3 : Composition API avec réactivité basée sur Signals

import { ref, computed, watchEffect } from 'vue';

// ref est essentiellement un Signal
const count = ref(0);

// computed est un Signal dérivé
const doubled = computed(() => count.value * 2);

// watchEffect est un Effect
watchEffect(() => {
  console.log('Count a changé:', count.value);
});

// Dans le template Vue
// <template>
//   <p>Count: {{ count }}</p>
//   <p>Doubled: {{ doubled }}</p>
//   <button @click="count++">Increment</button>
// </template>

Angular

// Angular 17+ : Signals officiels

import { signal, computed, effect } from '@angular/core';

@Component({
  template: `
    <p>Count: {{ count() }}</p>
    <p>Doubled: {{ doubled() }}</p>
    <button (click)="increment()">Increment</button>
  `
})
export class CounterComponent {
  // Signal
  count = signal(0);

  // Computed
  doubled = computed(() => this.count() * 2);

  constructor() {
    // Effect
    effect(() => {
      console.log('Count a changé:', this.count());
    });
  }

  increment() {
    this.count.update(c => c + 1);
    // ou : this.count.set(this.count() + 1);
  }
}

Preact Signals

// Preact Signals : fonctionne aussi dans React !

import { signal, computed, effect } from '@preact/signals-react';

// Signals globaux (en dehors du composant)
const count = signal(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  // Pas de hooks ! Signal est global et réactif
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => count.value++}>
        Increment
      </button>
    </div>
  );
}

// Le composant NE se re-rend PAS quand count change
// Seul le texte est mis à jour directement

La Proposition TC39

État en 2026

// TC39 Signals Proposal

const tc39Proposal = {
  stage: 'Stage 2 (brouillon)',
  champions: ['Rob Eisenberg (Angular)', 'Daniel Ehrenberg'],
  goal: 'Primitives standard pour la réactivité',

  apis: {
    // Signal basique
    'Signal.State': 'Valeur réactive mutable',
    'Signal.Computed': 'Valeur dérivée (read-only)',
    'Signal.subtle.Watch': 'API bas niveau pour les effects',
  },

  notIncluded: {
    effect: 'Les frameworks implémentent',
    batch: 'Les frameworks implémentent',
    rendering: 'Les frameworks implémentent'
  },

  philosophy: 'Interopérabilité, pas remplacement'
};

API Proposée

// API TC39 (sujet à changements)

// Signal.State - valeur mutable
const count = new Signal.State(0);
count.get(); // 0
count.set(1);
count.get(); // 1

// Signal.Computed - valeur dérivée
const doubled = new Signal.Computed(() => count.get() * 2);
doubled.get(); // 2
// doubled.set() n'existe pas - c'est read-only

// Signal.subtle.Watch - pour les frameworks
const watcher = new Signal.subtle.Watch(() => {
  // Appelé quand les dépendances changent
  console.log('Quelque chose a changé');
});

// Les frameworks utilisent ceci pour implémenter les effects

Pourquoi Standardiser

// Le problème de la fragmentation actuelle

const currentFragmentation = {
  solidjs: 'createSignal() retourne un tuple',
  vue: 'ref() utilise .value',
  angular: 'signal() utilise des fonctions get/set',
  preact: 'signal() utilise .value',
  svelte5: 'Runes avec $state',
  qwik: 'useSignal() avec .value'
};

// Avec TC39 Signals
const futureInterop = {
  sharedPrimitive: 'Signal.State et Signal.Computed',
  frameworks: 'Wrappers fins sur les primitives standard',
  libraries: 'Peuvent être framework-agnostic',
  benefit: 'Partager du code réactif entre frameworks'
};

Signals vs useState

Quand Utiliser Chacun

// React avec useState (standard actuel)

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [errors, setErrors] = useState({});

  // Chaque setState cause un re-render

  return (
    <form>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      {/* Re-rend tout à chaque frappe */}
    </form>
  );
}

// Avec Signals (ex : Preact Signals dans React)

const name = signal('');
const email = signal('');
const errors = signal({});

function Form() {
  // Le composant ne se re-rend pas !

  return (
    <form>
      <input
        value={name}
        onInput={e => name.value = e.target.value}
      />
      <input
        value={email}
        onInput={e => email.value = e.target.value}
      />
      {/* Updates granulaires, pas de re-render */}
    </form>
  );
}

Trade-offs

// Quand les Signals brillent
const signalsIdealFor = [
  'Formulaires avec beaucoup de champs',
  'Animations et transitions',
  'Updates haute fréquence',
  'État partagé entre composants',
  'Apps avec beaucoup d\'interactivité'
];

// Quand useState est suffisant
const useStateIdealFor = [
  'État local simple',
  'UIs avec peu de changements',
  'Quand le re-render est peu coûteux',
  'Quand tu as besoin d\'effects sur le re-render'
];

// Considérations
const considerations = {
  devex: 'Signals nécessite un modèle mental différent',
  debugging: 'useState plus facile à débuguer',
  ecosystem: 'L\'écosystème React suppose useState',
  future: 'React pourrait ajouter des signals natifs'
};

Implémenter les Signals Aujourd'hui

Dans React

// Option 1 : Preact Signals
npm install @preact/signals-react

import { signal, computed } from '@preact/signals-react';

// Option 2 : Jotai (signal-like)
npm install jotai

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubled] = useAtom(doubledAtom);
  // ...
}

// Option 3 : Legend State
npm install @legendapp/state @legendapp/state/react

import { observable } from '@legendapp/state';
import { observer } from '@legendapp/state/react';

const state = observable({ count: 0 });

const Counter = observer(function Counter() {
  return <div>{state.count.get()}</div>;
});

Dans Vue

// Vue 3 a déjà les signals (refs)

import { ref, computed, watch } from 'vue';

// Composable réactif
function useCounter() {
  const count = ref(0);
  const doubled = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  return { count, doubled, increment };
}

// Utilisation dans un composant
export default {
  setup() {
    const { count, doubled, increment } = useCounter();
    return { count, doubled, increment };
  }
};

Dans Svelte 5

// Svelte 5 Runes (signals)

<script>
  // $state est un signal
  let count = $state(0);

  // $derived est un computed
  let doubled = $derived(count * 2);

  // $effect est un effect
  $effect(() => {
    console.log('Count est', count);
  });

  function increment() {
    count++; // Aussi simple que ça !
  }
</script>

<button onclick={increment}>
  Count: {count}
</button>
<p>Doubled: {doubled}</p>

Le Futur Avec TC39 Signals

Scénario 2027+

// Futur possible avec Signals natifs

// JavaScript natif (sans imports !)
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);

// React pourrait adopter
function Counter() {
  // hook use() pour les signals
  const countValue = use(count);

  return <p>Count: {countValue}</p>;
}

// Ou même JSX natif
<p>Count: {count}</p> // Auto-unwrap dans JSX

// Bibliothèques framework-agnostic
class ReactiveDatePicker {
  selectedDate = new Signal.State(null);
  formattedDate = new Signal.Computed(() =>
    this.selectedDate.get()?.toLocaleDateString()
  );

  // Fonctionne dans React, Vue, Angular, Svelte...
}

Impact sur l'Écosystème

// Ce qui change avec les Signals natifs

const ecosystemImpact = {
  frameworks: {
    change: 'Wrappers fins sur les primitives natives',
    benefit: 'Moins de code, plus de performance',
    risk: 'Moins de différenciation entre frameworks'
  },

  libraries: {
    change: 'Peuvent être framework-agnostic',
    benefit: 'Écris une fois, utilise partout',
    example: 'Bibliothèque de formulaires qui fonctionne dans React et Vue'
  },

  learning: {
    change: 'Le concept de signal devient fondamental',
    benefit: 'Transfert de connaissances entre frameworks',
    requirement: 'Comprendre la réactivité devient essentiel'
  },

  performance: {
    change: 'Optimisations dans le moteur JavaScript',
    benefit: 'Signals plus rapides que toute implémentation userland',
    expectation: 'V8, SpiderMonkey optimisent pour les signals'
  }
};

Conclusion

Les Signals représentent un changement fondamental dans notre façon de penser la réactivité sur le web. Alors que le modèle de re-render de React a dominé la dernière décennie, les signals offrent une alternative plus granulaire et performante.

Que faire maintenant :

  1. Comprends le concept : Même sans les utiliser, comprends comment fonctionnent les signals
  2. Expérimente : Essaie Preact Signals, Vue 3 ou Angular 17+
  3. Suis le TC39 : La proposition avance
  4. N'abandonne pas React : Les signals peuvent compléter, pas remplacer

Le futur semble converger vers les signals comme primitive standard de réactivité. Les frameworks continueront d'exister, mais avec une base commune qui facilite l'interopérabilité et l'apprentissage.

Pour en savoir plus sur l'écosystème JavaScript actuel, lis : VoidZero en 2026.

Allons-y ! 🦅

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires