WellAlly Logo
WellAlly康心伴
Development

Building a Real-Time Heart Rate Monitor with React and the Web Bluetooth API

A step-by-step guide on how to connect a browser-based React application directly to a BLE heart rate strap without a backend using the Web Bluetooth API.

W
2025-12-10
8 min read
  • We will build a React app that connects directly to a Bluetooth Heart Rate strap.
  • No backend required: we use the browser's Web Bluetooth API.
  • You'll learn how to parse binary data streams using DataView.
  • We'll handle connection states and permissions gracefully.

Introduction

For years, connecting hardware devices to web applications required complex native wrappers, specialized plugins, or a backend server acting as a bridge. Enter the Web Bluetooth API.

It allows your browser (specifically Chrome, Edge, and Opera) to communicate directly with Bluetooth Low Energy (BLE) devices. This opens up a world of possibilities for HealthTech, IoT, and educational tools running entirely on the client side.

In this tutorial, we’re going to build a Real-Time Heart Rate Monitor. We will connect a standard BLE heart rate strap (like a Polar H10 or a generic monitor) to a React application, decode the binary data, and visualize your pulse.

Understanding the Problem

How BLE Works (The 30-Second Version)

Bluetooth Low Energy devices organize data into Services and Characteristics (this structure is called GATT).

  1. Server: The device (Heart Rate Strap).
  2. Service: A collection of data (e.g., Heart Rate Service: UUID 0x180D).
  3. Characteristic: The actual data point (e.g., Heart Rate Measurement: UUID 0x2A37).

The Challenge: The data coming from the device isn't JSON. It's a raw binary stream. We have to request permission, connect, listen for changes, and bit-mask the bytes to get human-readable numbers.

Prerequisites

  • A basic understanding of React (Hooks).
  • A Bluetooth Low Energy Heart Rate Monitor (or an emulator app like "nRF Connect" on your phone).
  • A computer with Bluetooth support.
  • Chrome or Edge browser (Firefox and Safari support is currently limited).

Step 1: Setting Up the Project

Let's initialize a simple React project. We'll use Vite for speed, but CRA works too.

code
npm create vite@latest heart-rate-monitor -- --template react
cd heart-rate-monitor
npm install
npm run dev
Code collapsed

We don't need external libraries for Bluetooth—it's native to the browser! However, we will use standard React hooks to manage our interface.

Step 2: Connection Logic

Security involves a "User Gesture." You cannot scan for Bluetooth devices automatically on page load; the user must click a button.

Create a file src/components/HeartRateMonitor.jsx:

code
import React, { useState, useRef } from 'react';

const HeartRateMonitor = () => {
  const [heartRate, setHeartRate] = useState(null);
  const [device, setDevice] = useState(null);
  const [status, setStatus] = useState('Disconnected');
  const [error, setError] = useState('');

  // We keep a ref to the device to handle disconnection cleanup
  const deviceRef = useRef(null);

  const connectToDevice = async () => {
    try {
      setStatus('Requesting device...');
      
      // 1. Request the device
      const device = await navigator.bluetooth.requestDevice({
        filters: [{ services: ['heart_rate'] }], // Standard HR Service UUID
        optionalServices: ['battery_service']    // Just in case we want battery later
      });

      deviceRef.current = device;
      setDevice(device);

      // 2. Connect to the GATT server
      setStatus('Connecting to GATT Server...');
      const server = await device.gatt.connect();

      // 3. Get the Heart Rate Service
      const service = await server.getPrimaryService('heart_rate');

      // 4. Get the Characteristic
      const characteristic = await service.getCharacteristic('heart_rate_measurement');

      // 5. Subscribe to notifications
      await characteristic.startNotifications();
      characteristic.addEventListener('characteristicvaluechanged', handleHeartRateChange);

      // Handle disconnection
      device.addEventListener('gattserverdisconnected', onDisconnected);
      
      setStatus('Connected');
      setError('');
    } catch (err) {
      console.error(err);
      setError(err.message);
      setStatus('Error');
    }
  };

  const onDisconnected = () => {
    setStatus('Disconnected');
    setHeartRate(null);
    setDevice(null);
  };

  const disconnectFromDevice = () => {
    if (deviceRef.current) {
      deviceRef.current.gatt.disconnect();
    }
  };

  // Placeholder for parsing logic (Step 3)
  const handleHeartRateChange = (event) => {
    console.log("Data received", event.target.value);
  };

  return (
    <div className="monitor-container">
      <h1>Web Bluetooth HR Monitor</h1>
      {error && <p className="error">{error}</p>}
      <p>Status: {status}</p>
      
      {!device ? (
        <button onClick={connectToDevice}>Connect to Heart Rate Monitor</button>
      ) : (
        <button onClick={disconnectFromDevice}>Disconnect</button>
      )}

      {heartRate && <div className="heart-rate">{heartRate} BPM</div>}
    </div>
  );
};

export default HeartRateMonitor;
Code collapsed

How it works

  1. navigator.bluetooth.requestDevice: Triggers the browser's native popup. We filter by heart_rate because we only want relevant devices.
  2. gatt.connect(): Establishes the link.
  3. startNotifications(): Tells the device "Hey, send me data whenever the heart rate changes" (usually once per second).

Step 3: Parsing Binary Data

This is the most critical part. The Bluetooth standard for Heart Rate (UUID 0x2A37) sends a stream of bytes. We need to parse them.

The first byte is the Flags byte.

  • Bit 0: Heart Rate Value Format bit.
    • 0 = Heart Rate is UINT8 (0-255 bpm).
    • 1 = Heart Rate is UINT16 (0-65535 bpm).

Update the handleHeartRateChange function in your component:

code
  const handleHeartRateChange = (event) => {
    const value = event.target.value;
    // value is a DataView object
    
    // 1. Parse the flags (first byte)
    const flags = value.getUint8(0);
    
    // 2. Determine format based on Bit 0
    // (flags & 1) checks if the first bit is 1
    const rate16Bits = (flags & 0x1) === 1;
    
    let heartRateMeasurement;
    
    if (rate16Bits) {
      // 16-bit heart rate (offset 1)
      heartRateMeasurement = value.getUint16(1, true /* littleEndian */);
    } else {
      // 8-bit heart rate (offset 1)
      heartRateMeasurement = value.getUint8(1);
    }
    
    setHeartRate(heartRateMeasurement);
  };
Code collapsed

Why do we need this?

Most consumer straps use 8-bit (uint8). However, medical-grade devices or specialized equipment might use 16-bit. If you just read the byte at index 1 without checking the flag, your app will break on high-end devices.

Step 4: Adding Visual Polish

Let's make it look like a real health app. Add a CSS pulse effect based on the connection status.

App.css:

code
.monitor-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-family: sans-serif;
  margin-top: 50px;
}

.heart-rate {
  font-size: 6rem;
  font-weight: bold;
  color: #e74c3c;
  margin: 20px 0;
  animation: pulse 1s infinite;
}

button {
  padding: 10px 20px;
  font-size: 1rem;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

button:hover {
  background-color: #2980b9;
}

.error {
  color: red;
  background: #fee;
  padding: 10px;
  border-radius: 4px;
}

@keyframes pulse {
  0% { transform: scale(1); }
  50% { transform: scale(1.05); }
  100% { transform: scale(1); }
}
Code collapsed

Putting It All Together

Here is the complete, integrated component.

code
// src/components/HeartRateMonitor.jsx
import React, { useState, useRef, useEffect } from 'react';
import './App.css'; // Assuming you put styles here

const HeartRateMonitor = () => {
  const [heartRate, setHeartRate] = useState(null);
  const [device, setDevice] = useState(null);
  const [status, setStatus] = useState('Disconnected');
  const [error, setError] = useState('');
  const deviceRef = useRef(null);

  // Clean up on unmount
  useEffect(() => {
    return () => {
      if (deviceRef.current) {
        deviceRef.current.gatt.disconnect();
      }
    };
  }, []);

  const connectToDevice = async () => {
    try {
      setError('');
      setStatus('Searching...');
      
      const device = await navigator.bluetooth.requestDevice({
        filters: [{ services: ['heart_rate'] }]
      });

      deviceRef.current = device;
      setDevice(device);

      device.addEventListener('gattserverdisconnected', onDisconnected);

      setStatus('Connecting...');
      const server = await device.gatt.connect();
      
      const service = await server.getPrimaryService('heart_rate');
      
      const characteristic = await service.getCharacteristic('heart_rate_measurement');
      
      await characteristic.startNotifications();
      characteristic.addEventListener('characteristicvaluechanged', handleHeartRateChange);
      
      setStatus('Connected');
    } catch (err) {
      console.error(err);
      setStatus('Disconnected');
      // Ignore "User cancelled" errors
      if (err.name !== 'NotFoundError') { 
         setError('Failed to connect: ' + err.message); 
      }
    }
  };

  const onDisconnected = () => {
    setStatus('Disconnected');
    setHeartRate(null);
    setDevice(null);
    // Optional: Auto-reconnect logic could go here
  };

  const handleHeartRateChange = (event) => {
    const value = event.target.value;
    const flags = value.getUint8(0);
    
    // Check Least Significant Bit for format
    const rate16Bits = (flags & 0x1) === 1;
    let heartRateMeasurement;
    
    if (rate16Bits) {
      heartRateMeasurement = value.getUint16(1, true);
    } else {
      heartRateMeasurement = value.getUint8(1);
    }
    
    setHeartRate(heartRateMeasurement);
  };

  const disconnect = () => {
    if (deviceRef.current) {
      deviceRef.current.gatt.disconnect();
    }
  };

  return (
    <div className="monitor-container">
      <h1>❤️ React HR Monitor</h1>
      
      <div className="status-indicator">
        Status: <strong>{status}</strong>
      </div>

      {error && <div className="error">{error}</div>}

      {heartRate !== null && (
        <div className="heart-rate">
          {heartRate} <span style={{fontSize: '2rem'}}>BPM</span>
        </div>
      )}

      <div className="controls">
        {!device ? (
          <button onClick={connectToDevice}>Scan for Device</button>
        ) : (
          <button onClick={disconnect} style={{backgroundColor: '#e74c3c'}}>
            Disconnect
          </button>
        )}
      </div>
      
      {!navigator.bluetooth && (
        <p className="warning">
          ⚠️ Your browser does not support Web Bluetooth. Try Chrome or Edge.
        </p>
      )}
    </div>
  );
};

export default HeartRateMonitor;
Code collapsed

Common Pitfalls & Security

  1. HTTPS is Mandatory: Web Bluetooth only works on secure contexts (HTTPS) or localhost. You cannot deploy this to an HTTP site.
  2. User Gesture: You cannot call requestDevice inside a useEffect on load. It must be triggered by a click event.
  3. Browser Support: Currently, this works best on Chrome, Edge, and Android (Chrome). iOS Safari does not support Web Bluetooth.
    • Workaround for iOS: Use the "Bluefy" Web BLE browser app to test your site on an iPhone.
  4. Device Compatibility: Ensure your BLE device is not currently connected to your phone's native system menu. BLE devices can usually only maintain one connection at a time.

Conclusion

You’ve just built a bridge between the physical world (your heart) and the digital world (React), without a single line of backend code.

This pattern can be extended to control lights, read thermometers, or interact with educational robots. The Web Bluetooth API turns the browser into a powerful hardware interface tool.

Next Steps for You:

  1. Try adding a real-time chart using recharts to visualize the heart rate over time.
  2. Read the "Energy Expended" field from the Bluetooth packet (it's often in the same data stream!).
  3. Experiment with the Battery Service to show the device's battery level.

Resources

Discussion Questions

  1. Have you ever used the Web Bluetooth API for a project? What was the hardest part?
  2. What security concerns do you have regarding websites connecting to local hardware?
  3. If you could connect any device to a web page, what would you build?
#

Article Tags

javascriptwebbluetoothreacthealthtech
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare TechnologySoftware DevelopmentUser ExperienceAI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey

© 2024 康心伴 WellAlly · Professional Health Management
Building a Real-Time Heart Rate Monitor with React and the Web Bluetooth API