Skip to content

Real-time Data

A live stock ticker that receives price updates over a WebSocket connection and renders them reactively. Demonstrates signals with external data sources and lifecycle cleanup.

  • Connect to a WebSocket for live price updates
  • Display a table of stock prices that update in real time
  • Show connection status (connected/disconnected)
  • Color-code price changes (green for up, red for down)
  • Clean up the WebSocket connection when the component unmounts
stores/ticker.ts
import { signal, computed } from "@whisq/core";
export interface Stock {
symbol: string;
price: number;
change: number;
}
export const stocks = signal<Map<string, Stock>>(new Map());
export const connected = signal(false);
export const stockList = computed(() =>
Array.from(stocks.value.values())
.sort((a, b) => a.symbol.localeCompare(b.symbol))
);
export const updateStock = (symbol: string, price: number) => {
const current = stocks.value.get(symbol);
const change = current ? price - current.price : 0;
const updated = new Map(stocks.value);
updated.set(symbol, { symbol, price, change });
stocks.value = updated;
};
hooks/useWebSocket.ts
import { onMount, onCleanup } from "@whisq/core";
import { connected, updateStock } from "../stores/ticker";
export function useTickerSocket(url: string) {
onMount(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
connected.value = true;
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
updateStock(data.symbol, data.price);
};
ws.onclose = () => {
connected.value = false;
};
onCleanup(() => {
ws.close();
});
});
}
components/Ticker.ts
import { component, div, h1, table, thead, tbody, tr, th, td, span, each } from "@whisq/core";
import { stockList, connected } from "../stores/ticker";
import { useTickerSocket } from "../hooks/useWebSocket";
const Ticker = component(() => {
useTickerSocket("wss://example.com/stocks");
return div({ class: "ticker" },
div({ class: "header" },
h1("Live Prices"),
span({
class: () => connected.value ? "status connected" : "status disconnected",
}, () => connected.value ? "Connected" : "Disconnected"),
),
table(
thead(
tr(
th("Symbol"),
th("Price"),
th("Change"),
),
),
tbody(
each(() => stockList.value, (stock) =>
tr(
td({ class: "symbol" }, stock.symbol),
td(() => `$${stock.price.toFixed(2)}`),
td({
class: () => stock.change > 0 ? "up" : stock.change < 0 ? "down" : "",
}, () => {
const sign = stock.change > 0 ? "+" : "";
return `${sign}${stock.change.toFixed(2)}`;
}),
)
),
),
),
);
});
export default Ticker;
main.ts
import { mount } from "@whisq/core";
import Ticker from "./components/Ticker";
mount(Ticker({}), document.getElementById("app")!);

1. External data feeds into signals

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
updateStock(data.symbol, data.price);
};

WebSocket messages update a signal. Every component reading that signal updates automatically — no event bus, no pubsub, no manual DOM updates.

2. Immutable Map updates

const updated = new Map(stocks.value);
updated.set(symbol, { symbol, price, change });
stocks.value = updated;

Creating a new Map triggers reactivity. Mutating the existing Map would not.

3. Lifecycle cleanup

onMount(() => {
const ws = new WebSocket(url);
// ...
onCleanup(() => {
ws.close();
});
});

onCleanup runs when the component unmounts, closing the WebSocket connection.

4. Reactive CSS classes

td({
class: () => stock.change > 0 ? "up" : stock.change < 0 ? "down" : "",
})

The class changes reactively based on the price change direction.

  • Signals with external data — WebSocket messages write to signals, UI reads them
  • onMount + onCleanup — connect on mount, disconnect on unmount
  • Immutable collectionsnew Map(old) for reactive updates
  • computed()stockList derives a sorted array from the Map signal
  • Reactive classes — CSS classes that change based on signal values