Voltar para o Blog

Signals em JavaScript 2026: O Futuro da Reatividade na Web

Olá HaWkers, uma das propostas mais aguardadas do TC39 está avançando em 2026: Signals nativos no JavaScript. Essa feature promete unificar como frameworks lidam com reatividade e pode mudar fundamentalmente a forma como construímos aplicações web.

Vamos explorar o que são Signals, como funcionam, e o impacto no ecossistema.

O Que São Signals

Conceito Fundamental

// Signal é um container reativo para valores

// Analogia simples
const traditionalVariable = 5; // Valor estático
// Se mudar, ninguém sabe

// Signal
const count = signal(5); // Valor reativo
// Quando mudar, dependentes são notificados automaticamente

Como Funcionam

// Implementação conceitual (não é a API final do TC39)

// 1. Criar um signal
const count = signal(0);

// 2. Ler o valor
console.log(count.value); // 0

// 3. Escrever um valor
count.value = 1; // Notifica dependentes

// 4. Computed (derivado de outros signals)
const doubled = computed(() => count.value * 2);
// doubled.value é sempre count.value * 2
// Recalcula automaticamente quando count muda

// 5. Effect (side effect reativo)
effect(() => {
  console.log(`Count is now: ${count.value}`);
});
// Executa sempre que count muda

Por Que Signals Importam

O Problema Atual

// React: re-render de componente inteiro

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

  // Quando count muda:
  // 1. Função Counter executa novamente
  // 2. Virtual DOM é criado
  // 3. Diff com DOM anterior
  // 4. Patches aplicados

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

// Mesmo que só count mudou, tudo re-renderiza
// Signals: update granular

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

  // Quando count muda:
  // 1. Só o texto que usa count.value atualiza
  // 2. Computed doubled recalcula
  // 3. Só o texto que usa doubled atualiza
  // 4. Nenhum re-render de componente

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

// Update cirúrgico, sem overhead

Comparativo de Performance

// Benchmark conceitual

const performanceComparison = {
  react: {
    update: 'Component re-render',
    complexity: 'O(tree size)',
    overhead: 'Virtual DOM diff',
    ideal: 'UIs com muitas mudanças coordenadas'
  },

  signals: {
    update: 'Direct DOM mutation',
    complexity: 'O(1) por signal',
    overhead: 'Tracking de dependências',
    ideal: 'Updates frequentes e granulares'
  },

  realWorld: {
    smallApp: 'Diferença negligível',
    largeApp: 'Signals 2-10x mais rápido',
    animations: 'Signals muito superior',
    forms: 'Signals muito superior'
  }
};

Frameworks Que Já Usam Signals

SolidJS

// SolidJS: Signals desde o início

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

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

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

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

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

// Note: count() é função, não .value
// SolidJS usa functions para tracking

Vue 3

// Vue 3: Composition API com reatividade baseada em Signals

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

// ref é essencialmente um Signal
const count = ref(0);

// computed é um Signal derivado
const doubled = computed(() => count.value * 2);

// watchEffect é um Effect
watchEffect(() => {
  console.log('Count changed:', count.value);
});

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

Angular

// Angular 17+: Signals oficiais

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 changed:', this.count());
    });
  }

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

Preact Signals

// Preact Signals: funciona em React também!

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

// Signals globais (fora do componente)
const count = signal(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  // Sem hooks! Signal é global e reativo
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => count.value++}>
        Increment
      </button>
    </div>
  );
}

// O componente NÃO re-renderiza quando count muda
// Apenas o texto é atualizado diretamente

A Proposta TC39

Status em 2026

// TC39 Signals Proposal

const tc39Proposal = {
  stage: 'Stage 2 (rascunho)',
  champions: ['Rob Eisenberg (Angular)', 'Daniel Ehrenberg'],
  goal: 'Primitivas padrão para reatividade',

  apis: {
    // Signal básico
    'Signal.State': 'Valor reativo mutável',
    'Signal.Computed': 'Valor derivado (read-only)',
    'Signal.subtle.Watch': 'API de baixo nível para effects',
  },

  notIncluded: {
    effect: 'Frameworks implementam',
    batch: 'Frameworks implementam',
    rendering: 'Frameworks implementam'
  },

  philosophy: 'Interoperabilidade, não substituição'
};

API Proposta

// API TC39 (sujeita a mudanças)

// Signal.State - valor mutável
const count = new Signal.State(0);
count.get(); // 0
count.set(1);
count.get(); // 1

// Signal.Computed - valor derivado
const doubled = new Signal.Computed(() => count.get() * 2);
doubled.get(); // 2
// doubled.set() não existe - é read-only

// Signal.subtle.Watch - para frameworks
const watcher = new Signal.subtle.Watch(() => {
  // Chamado quando dependências mudam
  console.log('Something changed');
});

// Frameworks usam isso para implementar effects

Por Que Padronizar

// O problema da fragmentação atual

const currentFragmentation = {
  solidjs: 'createSignal() retorna tuple',
  vue: 'ref() usa .value',
  angular: 'signal() usa funções get/set',
  preact: 'signal() usa .value',
  svelte5: 'Runes com $state',
  qwik: 'useSignal() com .value'
};

// Com TC39 Signals
const futureInterop = {
  sharedPrimitive: 'Signal.State e Signal.Computed',
  frameworks: 'Wrappers finos sobre primitivas padrão',
  libraries: 'Podem ser framework-agnostic',
  benefit: 'Compartilhar código reativo entre frameworks'
};

Signals vs useState

Quando Usar Cada Um

// React com useState (padrão atual)

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

  // Cada setState causa re-render

  return (
    <form>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      {/* Re-renderiza tudo a cada keystroke */}
    </form>
  );
}

// Com Signals (ex: Preact Signals em React)

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

function Form() {
  // Componente não re-renderiza!

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

Trade-offs

// Quando Signals brilham
const signalsIdealFor = [
  'Forms com muitos campos',
  'Animações e transições',
  'Updates de alta frequência',
  'Estado compartilhado entre componentes',
  'Apps com muita interatividade'
];

// Quando useState é suficiente
const useStateIdealFor = [
  'Estado local simples',
  'UIs com poucas mudanças',
  'Quando re-render é barato',
  'Quando você precisa de efeitos no re-render'
];

// Considerações
const considerations = {
  devex: 'Signals requer mental model diferente',
  debugging: 'useState mais fácil de debugar',
  ecosystem: 'React ecosystem assume useState',
  future: 'React pode adicionar signals nativos'
};

Implementando Signals Hoje

Em React

// Opção 1: Preact Signals
npm install @preact/signals-react

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

// Opção 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);
  // ...
}

// Opção 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>;
});

Em Vue

// Vue 3 já tem signals (refs)

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

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

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

  return { count, doubled, increment };
}

// Uso em componente
export default {
  setup() {
    const { count, doubled, increment } = useCounter();
    return { count, doubled, increment };
  }
};

Em Svelte 5

// Svelte 5 Runes (signals)

<script>
  // $state é um signal
  let count = $state(0);

  // $derived é um computed
  let doubled = $derived(count * 2);

  // $effect é um effect
  $effect(() => {
    console.log('Count is', count);
  });

  function increment() {
    count++; // Simples assim!
  }
</script>

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

O Futuro Com TC39 Signals

Cenário 2027+

// Possível futuro com Signals nativos

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

// React poderia adotar
function Counter() {
  // use() hook para signals
  const countValue = use(count);

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

// Ou até JSX nativo
<p>Count: {count}</p> // Auto-unwrap em JSX

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

  // Funciona em React, Vue, Angular, Svelte...
}

Impacto no Ecossistema

// O que muda com Signals nativos

const ecosystemImpact = {
  frameworks: {
    change: 'Wrappers finos sobre primitivas nativas',
    benefit: 'Menos código, mais performance',
    risk: 'Diferenciação menor entre frameworks'
  },

  libraries: {
    change: 'Podem ser framework-agnostic',
    benefit: 'Escreva uma vez, use em qualquer lugar',
    example: 'Form library que funciona em React e Vue'
  },

  learning: {
    change: 'Conceito de signal vira fundamento',
    benefit: 'Transferência de conhecimento entre frameworks',
    requirement: 'Entender reatividade se torna essencial'
  },

  performance: {
    change: 'Otimizações no engine JavaScript',
    benefit: 'Signals mais rápidos que qualquer userland',
    expectation: 'V8, SpiderMonkey otimizam para signals'
  }
};

Conclusão

Signals representam uma mudança fundamental em como pensamos sobre reatividade na web. Enquanto o modelo de re-render do React dominou a última década, signals oferecem uma alternativa mais granular e performática.

O que fazer agora:

  1. Entenda o conceito: Mesmo sem usar, entenda como signals funcionam
  2. Experimente: Teste Preact Signals, Vue 3, ou Angular 17+
  3. Acompanhe TC39: A proposta está avançando
  4. Não abandone React: Signals podem complementar, não substituir

O futuro parece convergir para signals como primitiva padrão de reatividade. Frameworks continuarão existindo, mas com uma base comum que facilita interoperabilidade e aprendizado.

Para entender mais sobre o ecossistema JavaScript atual, leia: VoidZero em 2026.

Bora pra cima! 🦅

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário