Building a Real-Time Dashboard with Reactive Programming

Hey there! Today we're going to build a real-time crypto dashboard that updates live as prices change. I'll show you how to handle WebSockets the right way using reactive programming.

The complete source code for this tutorial is available in my GitHub repository.


What We're Building#

We'll create a dashboard that shows:

  • Live cryptocurrency prices from Binance WebSockets
  • Price change indicators (green for rising, red for falling)
  • Volume indicators using simple visualizations
  • Connection status monitor with auto-reconnect
  • Price alerts when crypto prices jump or drop suddenly

This project brings together several reactive programming concepts in a real-world application:

  1. Handling WebSocket connections with automatic reconnection
  2. Transforming data streams with operators
  3. Creating derived streams for alerts and UI updates
  4. Sharing a single data source among multiple components

Don't worry if some of this sounds complex - I'll break it down into bite-sized pieces!

Prerequisites#

To follow along, you should:

  • Know JavaScript basics and ES6 features
  • Understand the basic concepts of reactive programming (observables, subscribers)
  • Have Node.js and npm installed

If you're new to reactive programming, check out my introduction to reactive programming post first.

Setting Up the Project#

Let's start by setting up our project structure:

code9 lines
1dashboard/
2  ├── package.json
3  ├── webpack.config.js
4  ├── index.html
5  ├── src/
6  │   ├── index.js
7  │   ├── websocket-service.js
8  │   └── dashboard.js
9  └── .gitignore

First, create a new folder and set up our project:

bash3 lines
1mkdir crypto-dashboard
2cd crypto-dashboard
3npm init -y

Now let's install the stuff we need:

bash2 lines
1npm install rxjs webpack webpack-cli webpack-dev-server html-webpack-plugin
2npm install --save-dev @babel/core @babel/preset-env babel-loader

Don't worry too much about all these packages - they're just the basic toolkit we need to get our app running.

webpack.config.js#

js33 lines
1const path = require("path");
2const HtmlWebpackPlugin = require("html-webpack-plugin");
3
4module.exports = {
5  entry: "./src/index.js",
6  output: {
7    filename: "bundle.js",
8    path: path.resolve(__dirname, "dist"),
9  },
10  devServer: {
11    static: "./dist",
12    hot: true,
13  },
14  module: {
15    rules: [
16      {
17        test: /\.js$/,
18        exclude: /node_modules/,
19        use: {
20          loader: "babel-loader",
21          options: {
22            presets: ["@babel/preset-env"],
23          },
24        },
25      },
26    ],
27  },
28  plugins: [
29    new HtmlWebpackPlugin({
30      template: "./index.html",
31    }),
32  ],
33};

Nothing fancy in our webpack config - just the basics to get our app bundled and served with hot reloading.

index.html#

html148 lines
1<!DOCTYPE html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <title>Crypto Dashboard</title>
7    <style>
8      body {
9        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
10          Oxygen, Ubuntu, Cantarell, sans-serif;
11        max-width: 1200px;
12        margin: 0 auto;
13        padding: 20px;
14        background: #f5f5f5;
15        color: #333;
16      }
17
18      h1 {
19        text-align: center;
20        margin-bottom: 30px;
21      }
22
23      .dashboard {
24        display: grid;
25        grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
26        gap: 20px;
27      }
28
29      .crypto-card {
30        background: white;
31        border-radius: 10px;
32        padding: 20px;
33        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
34        transition: transform 0.2s;
35      }
36
37      .crypto-card:hover {
38        transform: translateY(-5px);
39      }
40
41      .price {
42        font-size: 24px;
43        font-weight: bold;
44      }
45
46      .price-change {
47        font-weight: bold;
48      }
49
50      .rising {
51        color: #4caf50;
52      }
53
54      .falling {
55        color: #f44336;
56      }
57
58      .volume-bar {
59        height: 10px;
60        margin-top: 10px;
61        background: #e0e0e0;
62        border-radius: 5px;
63        overflow: hidden;
64      }
65
66      .volume-indicator {
67        height: 100%;
68        background: #2196f3;
69        width: 0;
70        transition: width 0.5s ease-out;
71      }
72
73      .status-bar {
74        display: flex;
75        justify-content: space-between;
76        align-items: center;
77        margin-bottom: 20px;
78        background: white;
79        padding: 10px 20px;
80        border-radius: 10px;
81        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
82      }
83
84      #connection-status {
85        display: inline-block;
86        padding: 8px 16px;
87        border-radius: 20px;
88        background: #ccc;
89        cursor: pointer;
90      }
91
92      #connection-status.connected {
93        background: #4caf50;
94        color: white;
95      }
96
97      #connection-status.disconnected {
98        background: #f44336;
99        color: white;
100      }
101
102      #connection-status.reconnecting {
103        background: #ff9800;
104        color: white;
105      }
106
107      #alerts {
108        position: fixed;
109        bottom: 20px;
110        right: 20px;
111        width: 300px;
112      }
113
114      .alert {
115        background: white;
116        border-left: 5px solid #ff9800;
117        padding: 15px;
118        margin-top: 10px;
119        border-radius: 5px;
120        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
121        animation: slideIn 0.3s ease-out;
122      }
123
124      @keyframes slideIn {
125        from {
126          transform: translateX(100%);
127          opacity: 0;
128        }
129        to {
130          transform: translateX(0);
131          opacity: 1;
132        }
133      }
134    </style>
135  </head>
136  <body>
137    <h1>Crypto Dashboard</h1>
138
139    <div class="status-bar">
140      <div>Last updated: <span id="last-updated">--</span></div>
141      <div id="connection-status">Connecting...</div>
142    </div>
143
144    <div class="dashboard" id="dashboard"></div>
145
146    <div id="alerts"></div>
147  </body>
148</html>

This gives us a nice, clean look for our dashboard with styles for price cards, status indicators, and alerts. I've tried to keep the CSS simple but still make it look good.

Building the WebSocket Service#

Now comes the fun part! Let's create our WebSocket service. This is where reactive programming really shines:

src/websocket-service.js#

js141 lines
1import { Observable, Subject, interval, of, throwError } from "rxjs";
2import { webSocket } from "rxjs/webSocket";
3import {
4  catchError,
5  map,
6  switchMap,
7  filter,
8  retryWhen,
9  delay,
10  share,
11  takeUntil,
12} from "rxjs/operators";
13
14// In a real app, this would point to your actual WebSocket server
15const WS_ENDPOINT = "wss://stream.binance.com:9443/ws/!ticker@arr";
16
17export class WebSocketService {
18  constructor() {
19    // Subject for manually closing the connection
20    this.closeSubject = new Subject();
21
22    // Create connection status observable
23    this.connectionStatus$ = new Subject();
24
25    // Create a WebSocket subject that can multicast to multiple subscribers
26    this.socket$ = webSocket({
27      url: WS_ENDPOINT,
28      openObserver: {
29        next: () => {
30          console.log("WebSocket connected!");
31          this.connectionStatus$.next("connected");
32        },
33      },
34      closeObserver: {
35        next: () => {
36          console.log("WebSocket closed");
37          this.connectionStatus$.next("disconnected");
38        },
39      },
40    });
41
42    // Create shared, auto-reconnecting data stream
43    this.data$ = this.socket$.pipe(
44      // Retry with exponential backoff - this is crucial for production
45      // After hours of debugging flaky connections, I found this pattern works best
46      retryWhen((errors) =>
47        errors.pipe(
48          delay(1000), // Wait 1 second before trying again
49          map((error, i) => {
50            if (i >= 5) {
51              // If we've retried 5 times and still failing, give up
52              throw error; // This will be caught by the catchError below
53            }
54            console.log(`Retrying connection (${i + 1})...`);
55            this.connectionStatus$.next("reconnecting");
56            return i;
57          })
58        )
59      ),
60      // Filter out non-array responses - Binance sometimes sends heartbeats/other data
61      filter((data) => Array.isArray(data)),
62      // Only take data until someone explicitly calls close()
63      takeUntil(this.closeSubject),
64      // Process the incoming data
65      map((data) => this.processBinanceData(data)),
66      // Always add error handling - don't let errors bubble up and break your UI!
67      catchError((error) => {
68        console.error("WebSocket error:", error);
69        this.connectionStatus$.next("error");
70        // Return empty result instead of error to keep the stream alive
71        return of({ cryptos: [], timestamp: Date.now() });
72      }),
73      // This is KEY: share() turns a cold observable hot and multicasts to all subscribers
74      // Without this, each component subscribing would create its own WebSocket!
75      share()
76    );
77
78    // Set up heartbeat to detect disconnects that the browser missed
79    // This was a hard-won lesson from production - browsers don't always fire onclose!
80    this.heartbeat$ = interval(30000).pipe(
81      takeUntil(this.closeSubject),
82      switchMap(() => {
83        if (this.socket$.closed) {
84          console.log("Socket closed, attempting to reconnect");
85          return throwError(() => new Error("Disconnected"));
86        }
87        return of(null);
88      }),
89      catchError(() => {
90        this.reconnect();
91        return of(null);
92      })
93    );
94
95    // Start the heartbeat
96    this.heartbeat$.subscribe();
97  }
98
99  // Process Binance data format into our app format
100  processBinanceData(data) {
101    // We're only interested in a few major cryptocurrencies
102    const tickers = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "ADAUSDT"];
103    const filtered = data.filter((item) => tickers.includes(item.s));
104
105    return {
106      cryptos: filtered.map((item) => ({
107        symbol: item.s.replace("USDT", ""),
108        price: parseFloat(item.c),
109        priceChange: parseFloat(item.P),
110        volume: parseFloat(item.v),
111        // Calculate a volume score from 1-10 for visualization
112        volumeScore: Math.min(10, Math.ceil(Math.log(parseFloat(item.v)) / 10)),
113      })),
114      timestamp: Date.now(),
115    };
116  }
117
118  // Method to get data as an observable
119  getData() {
120    return this.data$;
121  }
122
123  // Get connection status as observable
124  getConnectionStatus() {
125    return this.connectionStatus$.asObservable();
126  }
127
128  // Manual reconnect method
129  reconnect() {
130    this.socket$.complete();
131    this.socket$.connect();
132    this.connectionStatus$.next("connecting");
133  }
134
135  // Clean close of the WebSocket
136  close() {
137    this.closeSubject.next();
138    this.closeSubject.complete();
139    this.socket$.complete();
140  }
141}

Okay, I know that looks like a lot of code! But let me break it down for you:

  1. We're setting up a WebSocket connection to get real-time crypto prices
  2. We add some smart retry logic - if the connection drops, we try again (but not forever)
  3. We share one connection between all parts of our app (super important!)
  4. We filter and transform the data to make it easier to use

Think of this service like a radio station. It broadcasts data, and different parts of our app can tune in without interfering with each other.

The coolest part? The share() operator. Without it, each part of our app would open its own connection - like everyone bringing their own radio to the same concert!

I learned the hard way that browsers sometimes don't tell you when a connection drops - like when your phone switches from WiFi to cellular. That's why we added the heartbeat - it's like regularly asking "Hey, you still there?" so we know when we need to reconnect.

Building the Dashboard Logic#

Now let's create the dashboard that shows our crypto prices. This file's a bit long, but I've added lots of comments:

src/dashboard.js#

js250 lines
1import { fromEvent, Subject, merge } from "rxjs";
2import {
3  map,
4  debounceTime,
5  distinctUntilChanged,
6  throttleTime,
7  takeUntil,
8  scan,
9  buffer,
10  switchMap,
11  tap,
12} from "rxjs/operators";
13
14export class Dashboard {
15  constructor(websocketService) {
16    this.websocketService = websocketService;
17    this.destroy$ = new Subject();
18    this.lastPrices = new Map();
19
20    // DOM references
21    this.dashboardEl = document.getElementById("dashboard");
22    this.lastUpdatedEl = document.getElementById("last-updated");
23    this.connectionStatusEl = document.getElementById("connection-status");
24    this.alertsEl = document.getElementById("alerts");
25
26    this.initialize();
27  }
28
29  initialize() {
30    // Observe connection status changes
31    this.websocketService
32      .getConnectionStatus()
33      .pipe(takeUntil(this.destroy$))
34      .subscribe((status) => {
35        this.updateConnectionStatus(status);
36      });
37
38    // Main data stream
39    const data$ = this.websocketService.getData();
40
41    // Update dashboard with latest prices
42    data$
43      .pipe(
44        takeUntil(this.destroy$)
45        // This is where reactive programming really helps - we can derive multiple streams
46        // from a single data source for different purposes
47      )
48      .subscribe((data) => {
49        this.updateDashboard(data);
50      });
51
52    // Create a separate stream just for price alerts
53    // This shows the power of creating derived streams with different operators
54    data$
55      .pipe(
56        takeUntil(this.destroy$),
57        // Use scan to keep track of previous values and detect big changes
58        // Think of scan like a snowball rolling downhill, gathering data as it goes
59        scan(
60          (acc, data) => {
61            const alerts = [];
62
63            data.cryptos.forEach((crypto) => {
64              const prev = acc.prices.get(crypto.symbol);
65              if (prev) {
66                // Calculate percent change since last update
67                const pctChange = ((crypto.price - prev) / prev) * 100;
68
69                // Alert on significant changes (more than 0.5% in a single update)
70                if (Math.abs(pctChange) > 0.5) {
71                  alerts.push({
72                    symbol: crypto.symbol,
73                    price: crypto.price,
74                    change: pctChange,
75                    isPositive: pctChange > 0,
76                  });
77                }
78              }
79
80              // Update our tracking map with latest price
81              acc.prices.set(crypto.symbol, crypto.price);
82            });
83
84            return {
85              prices: acc.prices,
86              alerts,
87            };
88          },
89          { prices: new Map(), alerts: [] }
90        ),
91        // Only proceed when there are alerts
92        map((result) => result.alerts),
93        filter((alerts) => alerts.length > 0)
94      )
95      .subscribe((alerts) => {
96        this.showAlerts(alerts);
97      });
98
99    // Create a separate stream for volume analysis
100    data$
101      .pipe(
102        takeUntil(this.destroy$),
103        map((data) => {
104          // Calculate total volume across all cryptos
105          const totalVolume = data.cryptos.reduce(
106            (sum, crypto) => sum + crypto.volume,
107            0
108          );
109          return {
110            totalVolume,
111            cryptos: data.cryptos,
112          };
113        })
114        // We could do sophisticated volume analysis here
115      )
116      .subscribe((volumeData) => {
117        // For now, we're just using this for our UI volume bars
118        // but in a real app, you might generate volume-based trading signals
119      });
120  }
121
122  updateDashboard(data) {
123    // Update "last updated" timestamp
124    const date = new Date(data.timestamp);
125    this.lastUpdatedEl.textContent = date.toLocaleTimeString();
126
127    // Update or create cards for each cryptocurrency
128    data.cryptos.forEach((crypto) => {
129      let cardEl = document.getElementById(`crypto-${crypto.symbol}`);
130
131      // If this crypto doesn't have a card yet, create one
132      if (!cardEl) {
133        cardEl = document.createElement("div");
134        cardEl.id = `crypto-${crypto.symbol}`;
135        cardEl.className = "crypto-card";
136        cardEl.innerHTML = `
137          <h2>${crypto.symbol}</h2>
138          <div class="price">$${crypto.price.toFixed(2)}</div>
139          <div class="price-change ${
140            crypto.priceChange >= 0 ? "rising" : "falling"
141          }">
142            ${crypto.priceChange >= 0 ? "â–²" : "â–¼"} ${Math.abs(
143          crypto.priceChange
144        ).toFixed(2)}%
145          </div>
146          <div class="volume">
147            Volume: ${this.formatVolume(crypto.volume)}
148            <div class="volume-bar">
149              <div class="volume-indicator" style="width: ${
150                crypto.volumeScore * 10
151              }%"></div>
152            </div>
153          </div>
154        `;
155        this.dashboardEl.appendChild(cardEl);
156      } else {
157        // Update existing card
158        const priceEl = cardEl.querySelector(".price");
159        const priceChangeEl = cardEl.querySelector(".price-change");
160        const volumeBarEl = cardEl.querySelector(".volume-indicator");
161
162        // Check if price changed to add flash effect
163        const prevPrice = this.lastPrices.get(crypto.symbol) || crypto.price;
164        const priceChanged = prevPrice !== crypto.price;
165
166        if (priceChanged) {
167          // Add flash effect class based on price direction
168          const flashClass = crypto.price > prevPrice ? "rising" : "falling";
169          priceEl.classList.add(flashClass);
170
171          // Remove flash effect after animation completes
172          setTimeout(() => priceEl.classList.remove(flashClass), 1000);
173        }
174
175        // Update values
176        priceEl.textContent = `$${crypto.price.toFixed(2)}`;
177        priceChangeEl.textContent = `${
178          crypto.priceChange >= 0 ? "â–²" : "â–¼"
179        } ${Math.abs(crypto.priceChange).toFixed(2)}%`;
180        priceChangeEl.className = `price-change ${
181          crypto.priceChange >= 0 ? "rising" : "falling"
182        }`;
183        volumeBarEl.style.width = `${crypto.volumeScore * 10}%`;
184      }
185
186      // Store current price for next comparison
187      this.lastPrices.set(crypto.symbol, crypto.price);
188    });
189  }
190
191  updateConnectionStatus(status) {
192    this.connectionStatusEl.className = status;
193
194    switch (status) {
195      case "connected":
196        this.connectionStatusEl.textContent = "Connected";
197        break;
198      case "disconnected":
199        this.connectionStatusEl.textContent =
200          "Disconnected - Click to reconnect";
201        break;
202      case "reconnecting":
203        this.connectionStatusEl.textContent = "Reconnecting...";
204        break;
205      case "connecting":
206        this.connectionStatusEl.textContent = "Connecting...";
207        break;
208      default:
209        this.connectionStatusEl.textContent =
210          "Connection Error - Click to retry";
211        break;
212    }
213  }
214
215  showAlerts(alerts) {
216    alerts.forEach((alert) => {
217      const alertEl = document.createElement("div");
218      alertEl.className = "alert";
219      alertEl.innerHTML = `
220        <strong>${alert.symbol}</strong> ${alert.isPositive ? "up" : "down"} 
221        ${Math.abs(alert.change).toFixed(2)}% to $${alert.price.toFixed(2)}
222      `;
223
224      this.alertsEl.appendChild(alertEl);
225
226      // Remove alert after 5 seconds
227      setTimeout(() => {
228        if (alertEl.parentNode === this.alertsEl) {
229          alertEl.style.opacity = "0";
230          setTimeout(() => this.alertsEl.removeChild(alertEl), 300);
231        }
232      }, 5000);
233    });
234  }
235
236  formatVolume(volume) {
237    if (volume >= 1000000) {
238      return `${(volume / 1000000).toFixed(2)}M`;
239    } else if (volume >= 1000) {
240      return `${(volume / 1000).toFixed(2)}K`;
241    }
242    return volume.toFixed(2);
243  }
244
245  destroy() {
246    // Clean up all subscriptions
247    this.destroy$.next();
248    this.destroy$.complete();
249  }
250}

The dashboard might seem complex, but it's actually doing something really cool: creating three separate views from the same data stream!

  1. The main UI update stream shows the current prices
  2. The alert stream watches for sudden price jumps
  3. The volume stream tracks trading volume (which we could use for more analysis)

This is like having three different TV shows using footage from the same camera. Each show presents the same raw material in a different way.

My favorite trick here is using scan() to compare current prices with previous ones. Think of it like a person with a good memory - they can tell you not just the current price but how much it changed from before.

Tying It All Together#

Finally, let's connect everything in our index.js:

src/index.js#

js24 lines
1import { WebSocketService } from "./websocket-service.js";
2import { Dashboard } from "./dashboard.js";
3
4// Initialize the services when the DOM is ready
5document.addEventListener("DOMContentLoaded", () => {
6  console.log("Initializing dashboard...");
7
8  // Create the websocket service
9  const websocketService = new WebSocketService();
10
11  // Create the dashboard
12  const dashboard = new Dashboard(websocketService);
13
14  // Handle manual reconnect clicks
15  document.getElementById("connection-status").addEventListener("click", () => {
16    console.log("Manual reconnect requested");
17    websocketService.reconnect();
18  });
19
20  // Clean up on page unload
21  window.addEventListener("beforeunload", () => {
22    dashboard.destroy();
23  });
24});

This file is super simple. It just:

  1. Sets up our WebSocket service
  2. Creates our dashboard
  3. Adds a click handler to manually reconnect
  4. Makes sure we clean up when the page unloads

It's like the director of a play - making sure all the actors (our components) are in the right place and know their lines.

Running the Dashboard#

Add these scripts to your package.json:

json4 lines
1"scripts": {
2  "start": "webpack serve --mode development",
3  "build": "webpack --mode production"
4}

Then run:

bash1 lines
1npm start

Visit http://localhost:8080 and you'll see your reactive dashboard in action!

Troubleshooting Common Issues#

Every developer I know has hit these reactive programming gotchas at some point:

1. "My observable isn't doing anything!"#

This is almost always because you created the observable but didn't subscribe to it. Remember: observables are lazy - they don't do anything until you subscribe.

js7 lines
1// This won't work - nothing happens!
2webSocket("wss://example.com").pipe(map((data) => processData(data)));
3
4// This works - subscription triggers execution
5webSocket("wss://example.com")
6  .pipe(map((data) => processData(data)))
7  .subscribe((result) => console.log(result));

It's like setting up a camera but forgetting to press record!

2. "I'm getting multiple WebSocket connections!"#

If you see duplicate data or multiple connection messages, you probably forgot to use share(). Without it, each subscriber creates its own execution context.

This is like everyone in your family streaming the same Netflix show on different devices instead of watching it together on the TV.

3. "My WebSocket keeps disconnecting!"#

Network connections are flaky. Always implement:

  • Reconnection logic with retryWhen
  • Heartbeat detection for "zombie" connections
  • Status indicators in the UI so users know what's happening

Think of this like a long-distance relationship - you need regular check-ins and a backup plan for when calls drop!

4. "My UI is lagging with high-frequency data!"#

Too many updates can kill performance. Use throttling operators:

  • debounceTime - Wait for a pause in events
  • sample - Take latest value at regular intervals
  • throttleTime - Limit to one event per time window

This is like checking your email once an hour instead of every time a new message arrives.

5. "Memory usage keeps growing!"#

The classic memory leak. Usually caused by:

  • Not unsubscribing from observables when components are destroyed
  • Creating new subscriptions on every render/update
  • Keeping references to large datasets

It's like leaving the water running when you leave the house!

Real-World Lessons#

After building several reactive dashboards in production, here's what I've learned:

  1. Start simple, add complexity gradually - Begin with a basic stream and add operators one at a time, testing as you go.

  2. Centralize your socket handling - Create one service that manages connections and shares the data stream.

  3. Draw your streams before coding - I often sketch out my observable pipelines on paper first.

  4. Make error handling a priority - In a streaming app, errors are part of normal operation, not exceptional cases.

  5. Use the Chrome DevTools Memory panel - It's invaluable for tracking down RxJS-related memory leaks.

  6. Consider device capabilities - What works smoothly on your development machine might struggle on low-end devices.

  7. Add visibility into your streams - For debugging, I temporarily add .pipe(tap(x => console.log('Stream value:', x))) to see what's flowing through.

How It Works Under the Hood#

So what's going on behind the scenes that makes this approach so powerful?

Hot vs Cold Observables#

The WebSocket observable created by webSocket() is a hot observable - it emits values whether something is subscribed or not. This is perfect for real-time data feeds where we don't want to miss events.

Think of hot observables like a live concert - the band plays whether you're in the audience or not. Cold observables are more like a Netflix show - it starts playing when you press play.

When we apply share(), we're ensuring that we have exactly one WebSocket connection that multicasts its data to all subscribers. Without share(), each subscriber would create its own connection!

Derived Streams with Pure Functions#

We're creating several derived streams from our base data stream:

  • Main UI update stream
  • Price alert stream
  • Volume analysis stream

Each stream applies its own operators to the same base data. This is way cleaner than having multiple event handlers modifying shared state.

It's like cooking - you start with the same ingredients (raw data), but make different dishes (UI updates, alerts) without contaminating the original ingredients.

Declarative Error Handling#

Instead of try/catch blocks scattered throughout our code, we handle errors once at the observable level with catchError. This ensures our app stays responsive even when things go wrong.

It's like having one person assigned to handle emergencies instead of everyone panicking when something breaks.

Memory Management#

We're using the takeUntil(this.destroy$) pattern to ensure all our subscriptions get cleaned up when the dashboard is destroyed. This prevents memory leaks - a common problem with event-based systems.

Think of it like having one master switch that turns off all the lights when you leave the house.

Extending the Dashboard#

Here are some cool ways you could take this project further:

  1. Add user-defined price alerts - Let users set price thresholds that trigger notifications
  2. Implement trading signals - Use technical indicators to suggest buy/sell opportunities
  3. Add candlestick charts - Use a library like Highcharts to visualize price movements
  4. Create multiple timeframes - Add options to view different time intervals
  5. Add a news feed - Integrate cryptocurrency news using another API

Key Takeaways#

As you work with reactive programming for UI apps like this dashboard, remember:

  1. Think in streams - Data flows from sources through transformations to UI updates
  2. Share connections - Use share() to prevent duplicate WebSockets
  3. Clean up resources - Always unsubscribe or use takeUntil
  4. Handle errors gracefully - Network issues are normal, not exceptional
  5. Derive don't mutate - Create new streams instead of modifying shared state

References & Resources#

Official Documentation#

Tutorials & Articles#

Libraries & Tools#

  • RxJS - The reactive extensions library for JavaScript
  • D3.js - For more advanced visualizations
  • HighCharts - For financial charting

Example Projects#

Videos#

Source Code#

Final Thoughts#

Building reactive apps takes a different mindset, but once you get the hang of it, you'll find it's a much cleaner way to handle complex, real-time UIs. The dashboard we've built is just scratching the surface of what's possible.

The real power of reactive programming shines in applications like this one, where:

  1. Data arrives continuously and unpredictably
  2. Multiple parts of the UI need to respond to the same events
  3. Error handling and reconnection logic are crucial
  4. You need to make multiple views from the same base data

I hope this tutorial has given you a practical example of how reactive programming can solve real-world problems. Happy coding!

MB

Mehrshad Baqerzadegan

Sharing thoughts on technology and best practices.