React Integration
Use CloudSignal MQTT client in React applications
React Integration
Integrate CloudSignal into your React application with hooks and context.
Installation
npm install @cloudsignal/mqtt-client
Quick Start Hook
Create a reusable hook that handles connection, React StrictMode, and cleanup:
// hooks/useCloudSignal.ts
import { useEffect, useRef, useState, useCallback } from 'react';
import CloudSignal from '@cloudsignal/mqtt-client';
interface Message {
topic: string;
payload: unknown;
receivedAt: number;
}
export function useCloudSignal(options = {}) {
const {
debug = false,
tokenServiceUrl = 'https://auth.cloudsignal.app',
preset = 'desktop'
} = options;
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
// Refs prevent StrictMode double-connection
const clientRef = useRef(null);
const connectingRef = useRef(false);
const mountedRef = useRef(true);
const connect = useCallback(async (config) => {
// Guard against duplicate connections
if (connectingRef.current || clientRef.current) return;
connectingRef.current = true;
setIsConnecting(true);
setError(null);
try {
const client = new CloudSignal({ debug, preset, tokenServiceUrl });
client.onConnectionStatusChange = (connected) => {
if (mountedRef.current) setIsConnected(connected);
};
client.onAuthError = (err) => {
if (mountedRef.current) setError(err);
clientRef.current = null;
};
client.onMessage((topic, message) => {
const payload = (() => {
try { return JSON.parse(message); }
catch { return message; }
})();
setMessages(prev =>
[{ topic, payload, receivedAt: Date.now() }, ...prev].slice(0, 100)
);
});
await client.connectWithToken(config);
if (!mountedRef.current) {
client.destroy();
return;
}
clientRef.current = client;
} catch (err) {
if (mountedRef.current) setError(err);
} finally {
connectingRef.current = false;
if (mountedRef.current) setIsConnecting(false);
}
}, [debug, preset, tokenServiceUrl]);
const disconnect = useCallback(() => {
clientRef.current?.destroy();
clientRef.current = null;
setIsConnected(false);
}, []);
const subscribe = useCallback(async (topic, qos = 1) => {
await clientRef.current?.subscribe(topic, qos);
}, []);
const publish = useCallback((topic, message, options) => {
const payload = typeof message === 'string' ? message : JSON.stringify(message);
clientRef.current?.transmit(topic, payload, options);
}, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
clientRef.current?.destroy();
};
}, []);
return {
isConnected,
isConnecting,
error,
messages,
connect,
disconnect,
subscribe,
publish,
clearMessages: () => setMessages([]),
};
}
Usage Example
import { useCloudSignal } from './hooks/useCloudSignal';
import { useEffect } from 'react';
export function Dashboard({ session }) {
const { isConnected, messages, connect, subscribe, publish, error } = useCloudSignal({
debug: process.env.NODE_ENV === 'development',
});
// Connect on mount
useEffect(() => {
connect({
host: 'wss://connect.cloudsignal.app:18885/',
organizationId: 'your-org-uuid',
externalToken: session.access_token,
});
}, [session.access_token]);
// Subscribe when connected
useEffect(() => {
if (isConnected) {
subscribe('sensors/+/data');
}
}, [isConnected]);
return (
<div>
<p>Status: {isConnected ? 'π’ Connected' : 'π΄ Disconnected'}</p>
{error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg.topic}: {JSON.stringify(msg.payload)}</li>
))}
</ul>
<button onClick={() => publish('commands/led', { action: 'toggle' })}>
Toggle LED
</button>
</div>
);
}
Context Provider
For app-wide MQTT access, create a context provider:
// contexts/CloudSignalContext.tsx
import { createContext, useContext, ReactNode } from 'react';
import { useCloudSignal } from '../hooks/useCloudSignal';
const CloudSignalContext = createContext(null);
export function CloudSignalProvider({ children, ...options }) {
const cloudSignal = useCloudSignal(options);
return (
<CloudSignalContext.Provider value={cloudSignal}>
{children}
</CloudSignalContext.Provider>
);
}
export function useCloudSignalContext() {
const context = useContext(CloudSignalContext);
if (!context) {
throw new Error('useCloudSignalContext must be used within CloudSignalProvider');
}
return context;
}
Usage:
// App.tsx
import { CloudSignalProvider } from './contexts/CloudSignalContext';
function App() {
return (
<CloudSignalProvider
tokenServiceUrl="https://auth.cloudsignal.app"
debug={process.env.NODE_ENV === 'development'}
>
<Dashboard />
</CloudSignalProvider>
);
}
With Supabase Auth
Complete integration with automatic token refresh:
// hooks/useCloudSignalWithSupabase.ts
import { useEffect, useRef, useState, useCallback } from 'react';
import CloudSignal from '@cloudsignal/mqtt-client';
import { supabase } from '@/lib/supabase';
export function useCloudSignalWithSupabase({ autoConnect = true } = {}) {
const [isConnected, setIsConnected] = useState(false);
const [messages, setMessages] = useState([]);
const clientRef = useRef(null);
const connectingRef = useRef(false);
const mountedRef = useRef(true);
const connect = useCallback(async () => {
if (connectingRef.current || clientRef.current) return;
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) return;
connectingRef.current = true;
try {
const client = new CloudSignal({
tokenServiceUrl: 'https://auth.cloudsignal.app',
preset: 'desktop',
});
client.onConnectionStatusChange = (connected) => {
if (mountedRef.current) setIsConnected(connected);
};
client.onAuthError = () => {
clientRef.current = null;
};
client.onMessage((topic, message) => {
try {
setMessages(prev => [{ topic, payload: JSON.parse(message) }, ...prev].slice(0, 100));
} catch {
setMessages(prev => [{ topic, payload: message }, ...prev].slice(0, 100));
}
});
await client.connectWithToken({
host: 'wss://connect.cloudsignal.app:18885/',
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID,
externalToken: session.access_token,
});
if (!mountedRef.current) {
client.destroy();
return;
}
clientRef.current = client;
} finally {
connectingRef.current = false;
}
}, []);
const disconnect = useCallback(() => {
clientRef.current?.destroy();
clientRef.current = null;
setIsConnected(false);
}, []);
useEffect(() => {
mountedRef.current = true;
if (autoConnect) connect();
// Handle auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT') {
disconnect();
} else if (event === 'TOKEN_REFRESHED' && session) {
// Reconnect with new token
disconnect();
setTimeout(connect, 100);
} else if (event === 'SIGNED_IN' && !clientRef.current) {
connect();
}
});
return () => {
mountedRef.current = false;
subscription.unsubscribe();
disconnect();
};
}, [autoConnect, connect, disconnect]);
return {
isConnected,
messages,
subscribe: (topic) => clientRef.current?.subscribe(topic),
publish: (topic, msg) => clientRef.current?.transmit(topic, typeof msg === 'string' ? msg : JSON.stringify(msg)),
disconnect,
};
}
Chat Component Example
import { useState } from 'react';
import { useCloudSignalContext } from './contexts/CloudSignalContext';
export function ChatRoom() {
const { isConnected, messages, subscribe, publish } = useCloudSignalContext();
const [input, setInput] = useState('');
// Subscribe on mount
useEffect(() => {
if (isConnected) subscribe('chat/room/general');
}, [isConnected]);
const sendMessage = () => {
if (!input.trim()) return;
publish('chat/room/general', {
user: 'CurrentUser',
text: input,
timestamp: Date.now()
});
setInput('');
};
const chatMessages = messages.filter(m => m.topic === 'chat/room/general');
return (
<div>
<div>{isConnected ? 'π’ Connected' : 'π΄ Disconnected'}</div>
<div>
{chatMessages.map((msg, i) => (
<div key={i}>
<strong>{msg.payload.user}</strong>: {msg.payload.text}
</div>
))}
</div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === 'Enter' && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage} disabled={!isConnected}>Send</button>
</div>
);
}
Real-Time Notifications
import { useEffect } from 'react';
import { useCloudSignalContext } from './contexts/CloudSignalContext';
import { toast } from 'your-toast-library';
export function NotificationListener({ userId }) {
const { isConnected, messages, subscribe } = useCloudSignalContext();
useEffect(() => {
if (isConnected) {
subscribe(`notifications/user/${userId}`);
}
}, [isConnected, userId]);
// Show toast for new notifications
useEffect(() => {
const latest = messages[0];
if (latest?.topic.startsWith('notifications/')) {
toast[latest.payload.type || 'info'](latest.payload.body, {
title: latest.payload.title
});
}
}, [messages]);
return null;
}
Troubleshooting
React StrictMode Issues
If you see duplicate connections in development, ensure you use the ref guards shown above. React StrictMode intentionally mounts components twice.
Auth Errors
If onAuthError fires, check:
- Organization ID matches your dashboard
- External token (from Supabase/Firebase) hasn't expired
- Token service URL is
https://auth.cloudsignal.app
Connection Not Working
Enable debug mode to see detailed logs:
useCloudSignal({ debug: true })