Retour au blog

Signals en JavaScript: Le Futur de la Reactivite Qui Peut Changer le Web

Salut HaWkers, si vous travaillez avec des frameworks JavaScript modernes, vous avez probablement deja entendu parler de Signals. Ce qui a commence comme une approche alternative a la gestion d'etat devient un consensus parmi Angular, Vue, Solid, Svelte et potentiellement entrer dans la specification officielle de JavaScript.

Comprenons ce que sont les Signals, pourquoi ils conquierent la communaute et comment ils peuvent changer la facon dont nous ecrivons des applications web.

Que Sont les Signals

Concept Fondamental

Les Signals sont des primitives reactives qui stockent une valeur et notifient automatiquement leurs dependants quand cette valeur change. Contrairement au modele traditionnel de re-rendu complet, les Signals permettent des mises a jour chirurgicales uniquement la ou c'est necessaire.

Anatomie basique d'un Signal:

// Creer un signal
const count = signal(0)

// Lire la valeur
console.log(count()) // 0

// Mettre a jour la valeur
count.set(1)
// ou
count.update(n => n + 1)

// Deriver des valeurs (computed)
const doubled = computed(() => count() * 2)

// Reagir aux changements (effect)
effect(() => {
  console.log(`Count is: ${count()}`)
})

Pourquoi les Signals Sont Differents

La magie des Signals reside dans la reactivite fine (fine-grained reactivity). Comparez avec les approches traditionnelles:

React (re-render complet):

function Counter() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('User')

  // Quand count change, TOUT le composant re-rend
  // Y compris les parties qui dependent seulement de name
  return (
    <div>
      <h1>Hello, {name}</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  )
}

Avec Signals (update chirurgical):

function Counter() {
  const count = signal(0)
  const name = signal('User')

  // Quand count change, SEUL le texte du count se met a jour
  // Le h1 avec name n'est meme pas touche
  return (
    <div>
      <h1>Hello, {name()}</h1>
      <p>Count: {count()}</p>
      <button onClick={() => count.update(c => c + 1)}>+</button>
    </div>
  )
}

La Convergence des Frameworks

Qui Utilise les Signals

En 2026, pratiquement tous les frameworks JavaScript ont adopte ou adoptent les Signals.

Statut par framework:

Framework Statut Signals Depuis
Solid.js Natif (pionnier) 2021
Angular Signals API 2023
Vue 3.6 Alien Signals 2026
Svelte 5 Runes ($state) 2024
Preact @preact/signals 2022
Qwik Signals natifs 2022
React En discussion -

La Proposition TC39

Le plus interessant est que les Signals peuvent devenir partie de JavaScript natif. Il y a une proposition active au TC39 pour ajouter les Signals a la specification.

Statut de la proposition:

TC39 Proposal: Signals
Stage: 1 (Proposal)
Champions: Daniel Ehrenberg, Rob Riggs

Objectif: Creer une primitive de reactivite standardisee
qui fonctionne comme base pour tous les frameworks

Proposition d'API native:

// Possible API native (speculative)
const count = Signal.state(0)
const doubled = Signal.computed(() => count.get() * 2)

Signal.effect(() => {
  console.log(doubled.get())
})

count.set(5) // Logs: 10

Comment les Signals Fonctionnent en Interne

Le Systeme de Dependances

Les Signals utilisent un graphe de dependances qui est automatiquement construit pendant l'execution.

// En interne, quelque chose comme ca se passe:

class Signal {
  #value
  #subscribers = new Set()

  constructor(initialValue) {
    this.#value = initialValue
  }

  get() {
    // Si on est dans un effect/computed,
    // enregistre cette dependance
    if (currentTracking) {
      this.#subscribers.add(currentTracking)
    }
    return this.#value
  }

  set(newValue) {
    if (this.#value !== newValue) {
      this.#value = newValue
      // Notifie tous les subscribers
      this.#subscribers.forEach(sub => sub.notify())
    }
  }
}

Tracking Automatique

Le tracking des dependances se fait automatiquement quand vous lisez un signal dans un contexte reactif.

const firstName = signal('John')
const lastName = signal('Doe')
const age = signal(30)

// Ce computed depend SEULEMENT de firstName et lastName
// age n'est pas une dependance car il n'a pas ete lu
const fullName = computed(() => {
  return `${firstName()} ${lastName()}`
})

// Changer age NE recalcule PAS fullName
age.set(31) // fullName ne reagit pas

// Changer firstName recalcule fullName
firstName.set('Jane') // fullName recalcule

Push vs Pull

Les Signals utilisent un modele hybride push-pull:

// PUSH: Quand un signal change, il "pousse" notification
// a ses dependants directs

const a = signal(1)
const b = computed(() => a() * 2) // b depend de a
const c = computed(() => b() + 10) // c depend de b

a.set(2) // Push: a notifie b, b notifie c

// PULL: Les valeurs ne sont recalculees que quand lues
// (evaluation paresseuse)

console.log(c()) // Pull: c calcule b, b calcule a

Implementations Pratiques

Signals dans Angular

Angular a introduit les Signals comme alternative a RxJS pour les cas simples.

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

@Component({
  selector: 'app-counter',
  template: `
    <h1>Count: {{ count() }}</h1>
    <h2>Doubled: {{ doubled() }}</h2>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = signal(0)
  doubled = computed(() => this.count() * 2)

  constructor() {
    effect(() => {
      console.log(`Count changed to: ${this.count()}`)
    })
  }

  increment() {
    this.count.update(n => n + 1)
  }
}

Signals dans Solid.js

Solid a ete le pionnier des Signals modernes.

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

function Counter() {
  const [count, setCount] = createSignal(0)
  const doubled = createMemo(() => count() * 2)

  createEffect(() => {
    console.log(`Count is ${count()}`)
  })

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

Signals dans Vue 3.6

Vue introduit les Signals a travers le Vapor Mode.

<script setup>
import { signal, computed, effect } from 'vue/vapor'

const count = signal(0)
const doubled = computed(() => count() * 2)

effect(() => {
  console.log(`Count: ${count()}`)
})

function increment() {
  count.set(count() + 1)
}
</script>

<template>
  <div>
    <p>Count: {{ count() }}</p>
    <p>Doubled: {{ doubled() }}</p>
    <button @click="increment">+</button>
  </div>
</template>

Patterns Avances avec Signals

Signals Derives Complexes

// Dependances multiples
const items = signal([
  { name: 'Apple', price: 1.5, qty: 3 },
  { name: 'Banana', price: 0.75, qty: 5 },
])

const taxRate = signal(0.1)

// Computed qui depend de items et taxRate
const total = computed(() => {
  const subtotal = items().reduce(
    (sum, item) => sum + item.price * item.qty,
    0
  )
  return subtotal * (1 + taxRate())
})

// Mettre a jour n'importe quelle dependance recalcule total
items.update(i => [...i, { name: 'Orange', price: 2, qty: 2 }])

Effects avec Cleanup

// Les effects peuvent retourner une fonction de cleanup
const userId = signal(1)

effect(() => {
  const id = userId()

  // Setup: connecter WebSocket
  const ws = new WebSocket(`/user/${id}/updates`)

  ws.onmessage = (event) => {
    console.log('Update:', event.data)
  }

  // Cleanup: deconnecter quand userId change
  return () => {
    ws.close()
  }
})

userId.set(2) // Ferme ancien WS, ouvre nouveau

Batching des Updates

// Updates multiples en sequence
const a = signal(1)
const b = signal(2)
const c = signal(3)

const sum = computed(() => a() + b() + c())

effect(() => {
  console.log('Sum:', sum())
})

// Sans batching: 3 recalculs
a.set(10)
b.set(20)
c.set(30)

// Avec batching: 1 recalcul
batch(() => {
  a.set(10)
  b.set(20)
  c.set(30)
})

Signals vs Autres Approches

Comparaison avec Redux

// Redux: Beaucoup de boilerplate
const store = createStore(reducer)
const mapStateToProps = state => ({ count: state.count })
const mapDispatchToProps = { increment }
connect(mapStateToProps, mapDispatchToProps)(Component)

// Signals: Droit au but
const count = signal(0)
const increment = () => count.update(n => n + 1)

Comparaison avec RxJS

// RxJS: Puissant mais complexe
const count$ = new BehaviorSubject(0)
const doubled$ = count$.pipe(map(n => n * 2))
doubled$.subscribe(console.log)
count$.next(5)

// Signals: Plus simple pour les cas courants
const count = signal(0)
const doubled = computed(() => count() * 2)
effect(() => console.log(doubled()))
count.set(5)

Quand Utiliser Chaque Approche

Scenario Meilleure Option
Etat local de composant Signals
Streams de donnees complexes RxJS
Etat global simple Signals
Etat global complexe Redux/Zustand
Donnees asynchrones Signals + async
Event sourcing RxJS

Performance des Signals

Benchmarks

Les Signals offrent une performance superieure dans les scenarios de mises a jour frequentes.

Test: 10 000 updates en cascade:

Approche Temps Memoire
Signals (Solid) 12ms 2.1MB
Signals (Preact) 15ms 2.4MB
Vue 3 (ref) 45ms 4.2MB
React (useState) 180ms 8.5MB
MobX 28ms 3.8MB

Pourquoi les Signals sont rapides:

  1. Pas de diff VDOM - Updates directs au DOM
  2. Tracking precis - Ne recalcule que le necessaire
  3. Evaluation paresseuse - Calcule seulement quand lu
  4. Batching automatique - Groupe les updates

Optimisations Courantes

// 1. Evitez les computeds inutiles
// Mauvais
const doubled = computed(() => count() * 2)
const quadrupled = computed(() => doubled() * 2) // Chainement

// Mieux
const quadrupled = computed(() => count() * 4)

// 2. Utilisez untrack pour lectures non reactives
import { untrack } from 'solid-js'

effect(() => {
  const currentCount = count()
  // Cette lecture NE cree PAS de dependance
  const config = untrack(() => configSignal())
  console.log(currentCount, config)
})

// 3. Memoize les calculs lourds
const expensiveResult = computed(() => {
  return items().map(item => heavyComputation(item))
}, { equals: deepEquals })

Le Futur des Signals

Standardisation TC39

Si la proposition TC39 est approuvee, nous aurons des Signals natifs en JavaScript.

Timeline estimee:

  • 2024: Stage 1 (actuel)
  • 2025: Stage 2 (Draft)
  • 2026-2027: Stage 3 (Candidate)
  • 2028+: Stage 4 (Finished)

Avantages de la standardisation:

  1. Interoperabilite entre frameworks
  2. Performance optimisee par les engines
  3. Moins de dependances externes
  4. API consistante

Impact sur les Frameworks

Avec des Signals natifs, les frameworks pourraient:

// Aujourd'hui: Chaque framework a son implementation
import { signal } from 'solid-js' // Solid
import { ref } from 'vue' // Vue
import { signal } from '@angular/core' // Angular

// Futur: Tous utilisent la meme base
import { Signal } from 'std:signals' // Natif

React et Signals

React n'a pas encore adopte officiellement les Signals, mais il y a des discussions.

Perspectives:

  • React Forget (compiler) resout une partie du probleme
  • Les Signals contrediraient le modele mental de React
  • Possible adoption partielle ou bibliotheque officielle
  • La communaute utilise deja @preact/signals-react

Conclusion

Les Signals representent une evolution naturelle dans la gestion d'etat JavaScript. La convergence des principaux frameworks autour de cette primitive, combinee avec la proposition TC39, suggere que les Signals seront une partie fondamentale du developpement web dans les annees a venir.

Points principaux:

  1. Les Signals offrent une reactivite fine et performante
  2. Angular, Vue, Solid, Svelte ont deja adopte
  3. La proposition TC39 peut rendre les Signals natifs
  4. Performance superieure aux approches traditionnelles
  5. API simple et intuitive

Recommandations:

  • Apprenez l'API Signals de votre framework
  • Essayez Solid.js pour comprendre les Signals purs
  • Suivez la proposition TC39
  • Considerez les Signals pour les nouveaux projets

Le futur de la reactivite web est en train d'etre defini maintenant, et les Signals sont au centre de cette transformation.

Pour en savoir plus sur l'evolution des frameworks, lisez: Vue 3.6 Vapor Mode: La Revolution de Performance.

Allez, on y va! 🦅

Commentaires (0)

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

Ajouter des commentaires