Build an ESP32 Sensor Dashboard in Expo for 2026

Build an ESP32 Sensor Dashboard in Expo for 2026

Dec 12, 2025
8 minute read

A phone dashboard feels simple once it works. Getting sensor data off an ESP32 and onto that screen is where most people lose time.

The easiest path is not the flashiest one. For a first ESP32 sensor dashboard, keep the board and your phone on the same Wi-Fi network, let the ESP32 return JSON over HTTP, and have an Expo app poll that endpoint every few seconds.

That setup is fast to build, easy to debug, and good enough for many hobby and prototype apps. After that, you can swap the transport layer without throwing away the UI.

Start with a simple data flow

For this tutorial, the ESP32 reads a sensor, hosts a tiny local web server, and returns values like temperature and humidity as JSON. Your Expo app calls that endpoint with fetch, stores the response in state, and renders a dashboard card for each reading.

This works well for a room monitor, a plant station, a fermentation tracker, or a small lab setup. If the phone and board are on the same network, you can go from raw sensor value to mobile UI with almost no infrastructure.

A cloud-backed design comes later. If you want historical storage, remote access, or alerts when you're away from home, you can add a backend or study projects like the GardenSense Expo app with Firebase and AWS. For now, local polling keeps the moving parts small, which matters when you're still proving the device and UI.

What you need before you start

You don't need much hardware for the first version. A DHT22 is common, cheap, and good enough for a clean demo, although any ESP32-supported sensor will work.

Here's the short list:

| Item | Recommended choice | Why it helps | | ------------- | ---------------------------------------- | ---------------------------------- | | Board | ESP32 or ESP32-S3 | Widely supported, built-in Wi-Fi | | Sensor | DHT22 | Easy temperature and humidity demo | | Mobile app | Expo SDK 55+ | Good 2026 baseline | | Firmware tool | Arduino IDE with ESP32 board package 3.x | Quick setup for beginners |

On the app side, create a TypeScript Expo project:

npx create-expo-app esp32-dashboard

This tutorial uses plain HTTP requests, so Expo Go is enough. That's an important advantage. You can test on a real phone without a custom native build.

If you later move to BLE, things change. Libraries such as @react-native-ble-plx/ble-plx work well in 2026, but they need an Expo development build or production build through EAS, not plain Expo Go.

ESP32 firmware: read the sensor and expose JSON

Wire the DHT22 to the ESP32, then flash a sketch that joins Wi-Fi and exposes a /sensor endpoint. The code below reads the sensor, guards against bad values, and returns valid JSON.

#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>


#define DHTPIN 4
#define DHTTYPE DHT22


const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";


DHT dht(DHTPIN, DHTTYPE);
WebServer server(80);


void handleSensor() {
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();


if (isnan(temperature) || isnan(humidity)) {
server.send(503, "application/json", "{\"error\":\"sensor_read_failed\"}");
return;
}


String body = "{";
body += "\"temperature\":" + String(temperature, 1) + ",";
body += "\"humidity\":" + String(humidity, 1) + ",";
body += "\"uptime\":" + String(millis() / 1000);
body += "}";


server.sendHeader("Cache-Control", "no-store");
server.send(200, "application/json", body);
}


void setup() {
Serial.begin(115200);
dht.begin();


WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}


Serial.println();
Serial.println(WiFi.localIP());


server.on("/sensor", handleSensor);
server.begin();
}


void loop() {
server.handleClient();
}

Open the Serial Monitor after flashing. The printed IP address is the address your Expo app needs. If you visit http://ESP32_IP/sensor in a browser on the same network, you should see something like {"temperature":24.3,"humidity":51.7,"uptime":98}.

Two small details matter here. First, the code returns numbers, not text labels mixed into a sentence. That makes parsing easy. Second, it never returns NaN, because NaN breaks JSON.

Build the Expo dashboard screen

Create app/index.tsx if you're using Expo Router, or replace App.tsx in a basic project. The screen below polls the ESP32 every two seconds and shows the latest readings.

!Hand holds smartphone showing minimalist dashboard with circular gauges and line charts in bright indoor light.

import { useEffect, useState } from 'react';
import { SafeAreaView, StyleSheet, Text, View } from 'react-native';


const DEVICE_URL = 'http://192.168.1.50/sensor';


type SensorData = {
temperature: number;
humidity: number;
uptime: number;
error?: string;
};


export default function HomeScreen() {
const [data, setData] = useState<SensorData | null>(null);
const [error, setError] = useState('');
const [updatedAt, setUpdatedAt] = useState('');


useEffect(() => {
let active = true;


const load = async () => {
try {
const res = await fetch(`${DEVICE_URL}?t=${Date.now()}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json: SensorData = await res.json();


if (!active) return;
if (json.error) throw new Error(json.error);


setData(json);
setError('');
setUpdatedAt(new Date().toLocaleTimeString());
} catch (e: any) {
if (active) setError(e.message || 'Request failed');
}
};


load();
const id = setInterval(load, 2000);
return () => {
active = false;
clearInterval(id);
};
}, []);


return (
<SafeAreaView style={styles.screen}>
<Text style={styles.title}>ESP32 Dashboard</Text>
<Text style={styles.subtle}>Last update: {updatedAt || 'waiting...'}</Text>


<View style={styles.card}>
<Text style={styles.label}>Temperature</Text>
<Text style={styles.value}>{data ? `${data.temperature} C` : '--'}</Text>
</View>


<View style={styles.card}>
<Text style={styles.label}>Humidity</Text>
<Text style={styles.value}>{data ? `${data.humidity} %` : '--'}</Text>
</View>


<View style={styles.card}>
<Text style={styles.label}>Uptime</Text>
<Text style={styles.value}>{data ? `${data.uptime} s` : '--'}</Text>
</View>


{!!error && <Text style={styles.error}>Error: {error}</Text>}
</SafeAreaView>
);
}


const styles = StyleSheet.create({
screen: { flex: 1, padding: 20, backgroundColor: '#0f172a' },
title: { fontSize: 28, fontWeight: '700', color: '#fff', marginBottom: 6 },
subtle: { color: '#94a3b8', marginBottom: 18 },
card: { backgroundColor: '#1e293b', padding: 18, borderRadius: 16, marginBottom: 12 },
label: { color: '#94a3b8', marginBottom: 6 },
value: { color: '#fff', fontSize: 24, fontWeight: '600' },
error: { color: '#fca5a5', marginTop: 10 }
});

The ?t=${Date.now()} part helps avoid stale responses during testing. Also, the app polls on a fixed interval, which is fine for a basic dashboard. If you're using an IoT-focused Expo starter, you can drop this screen into your current tabs and later add auth, device lists, or historical charts around it.

Use the ESP32's LAN IP, not localhost. On your phone, localhost points to the phone, not the microcontroller.

Common issues when the app won't update

The phone can't reach the ESP32

Start with the network. The phone and ESP32 must be on the same Wi-Fi network, and guest networks often block device-to-device traffic. Give the ESP32 a stable IP through your router's DHCP reservation if the address keeps changing.

Also test the endpoint in the phone's browser. If the browser can't load /sensor, Expo won't load it either.

The JSON parses on one request, then fails later

Bad sensor reads are a common cause. Some sensors return NaN or time out when wiring is loose, power is noisy, or the read interval is too aggressive. Return a clear error string from the ESP32, as shown above, and handle it in the app.

Keep the payload small. Don't send "Temp: 24C Humidity: 50%" as a single string if the UI needs separate values. Send fields the app can trust.

CORS and network rules feel inconsistent

On native iOS and Android, fetch doesn't usually hit browser-style CORS limits. On Expo web, it does, because the request runs in a browser. If the web version fails while mobile works, your issue is likely CORS, not Wi-Fi.

For web testing, add CORS headers on the ESP32 or put a small proxy in front of it. For phone testing, use a real device on local Wi-Fi and skip the browser layer.

The sensor reads, but the UI feels laggy

Polling every 500 ms is wasteful for slow-moving values like room temperature. Try one to five seconds. Your UI will still feel live, and the board will do less work.

When polling stops being enough

HTTP polling is a good first build, but it has limits. If you need near real-time graphs, two-way control, or many devices, pick a transport that matches the job.

Use WebSockets when you want a push-based local dashboard over Wi-Fi. Move to MQTT when devices need broker-based messaging, remote dashboards, or retained state. Pick BLE when you want direct phone-to-device communication without local network setup.

If BLE is your next step, this live BLE app guide for React Native and ESP32 is a useful reference, and this Expo BLE proof-of-concept repo shows the shape of a direct device flow. In Expo, BLE still means a development build, native permissions, and more testing across iOS and Android. In 2026, that path is solid, but it isn't as quick as fetch in Expo Go.

Conclusion

A working mobile dashboard doesn't need a big stack. Start with the ESP32 reading clean sensor values, expose them as JSON, and let Expo poll that endpoint on local Wi-Fi.

That path gives you a usable app fast, and it keeps the hard parts visible. Once the basics work, you can swap in MQTT, WebSockets, or BLE without rewriting the whole dashboard.