@smarthivelabs-devs/hive-socket-react
v1.1.0
Published
React SDK for Hive-Socket — real-time hooks and provider for web apps
Downloads
60
Readme
@smarthivelabs-devs/hive-socket-react
React SDK for Hive-Socket — real-time hooks and provider for Next.js and React web apps.
Install
npm install @smarthivelabs-devs/hive-socket-react socket.io-client
# or
yarn add @smarthivelabs-devs/hive-socket-react socket.io-client
# or
pnpm add @smarthivelabs-devs/hive-socket-react socket.io-clientPeer dependencies: react >= 18, socket.io-client ^4
Setup
1. Wrap your app with the provider
The provider manages the WebSocket connection lifecycle. It reconnects automatically when the token changes (e.g. after a token refresh).
Next.js App Router:
// app/providers.tsx
'use client';
import { HiveSocketProvider } from '@smarthivelabs-devs/hive-socket-react';
export function Providers({
token,
children,
}: {
token: string;
children: React.ReactNode;
}) {
return (
<HiveSocketProvider url="https://socket.smarthivelabs.dev" token={token}>
{children}
</HiveSocketProvider>
);
}// app/layout.tsx
import { getAccessToken } from '@/lib/auth'; // your SmartHive Auth call
import { Providers } from './providers';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const token = await getAccessToken();
return (
<html>
<body>
<Providers token={token}>{children}</Providers>
</body>
</html>
);
}The provider must be a Client Component. In App Router, always create a
providers.tsxwrapper as shown — do not add'use client'tolayout.tsx.
React (Vite / CRA):
// main.tsx
import { HiveSocketProvider } from '@smarthivelabs-devs/hive-socket-react';
root.render(
<HiveSocketProvider url="https://socket.smarthivelabs.dev" token={accessToken}>
<App />
</HiveSocketProvider>
);2. Use hooks anywhere inside the provider
import { useHiveSocket, useMessages, useNotifications } from '@smarthivelabs-devs/hive-socket-react';Hooks
useHiveSocket
Connection status and all send actions.
const {
status, // 'connecting' | 'connected' | 'disconnected'
socketId, // string | null
joinRoom, // (roomId: string) => void
leaveRoom, // (roomId: string) => void
sendMessage, // (roomId: string, type: string, payload: Record<string, unknown>) => void
updatePresence, // (status: 'online' | 'away' | 'offline') => void
fetchHistory, // (roomId: string, limit?: number, before?: string) => void
markNotificationRead, // (notificationId: string) => void
} = useHiveSocket();Connection status indicator:
function ConnectionStatus() {
const { status } = useHiveSocket();
return (
<span style={{ color: status === 'connected' ? 'green' : 'gray' }}>
{status === 'connected' ? 'Live' : 'Connecting...'}
</span>
);
}useMessages(roomId)
Subscribe to real-time messages in a room. Automatically joins on mount, fetches the last 50 messages as history, appends new messages as they arrive, and leaves the room on unmount.
const messages = useMessages(roomId); // HiveMessage[]import { useMessages, useHiveSocket } from '@smarthivelabs-devs/hive-socket-react';
import { useState } from 'react';
function ChatRoom({ roomId }: { roomId: string }) {
const { status, sendMessage } = useHiveSocket();
const messages = useMessages(roomId);
const [text, setText] = useState('');
const send = () => {
if (!text.trim()) return;
sendMessage(roomId, 'chat.message', { text });
setText('');
};
return (
<div>
<div className="messages">
{messages.map((msg) => (
<div key={msg._eid}>
<strong>{msg.senderId}</strong>
<span>{msg.payload.text as string}</span>
<time>{new Date(msg.timestamp).toLocaleTimeString()}</time>
</div>
))}
</div>
<div className="input-row">
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && send()}
placeholder="Type a message..."
/>
<button onClick={send} disabled={status !== 'connected'}>
Send
</button>
</div>
</div>
);
}HiveMessage shape:
interface HiveMessage {
type: string; // e.g. 'chat.message', 'quiz.answer', 'vote.cast'
senderId: string; // userId of the sender
roomId: string;
payload: Record<string, unknown>; // your custom data
timestamp: number; // Unix ms
_eid: string; // unique event ID — use as React key
}Message type conventions:
| type | Use case |
|--------|----------|
| chat.message | Regular chat message |
| quiz.answer | Quiz submission |
| vote.cast | Vote event |
| doc.update | Collaborative document edit |
| presence.note | Presence-related message |
usePresence(userId)
Track another user's live presence status.
const presence = usePresence(userId);
// { status: 'online' | 'away' | 'offline', lastSeen: string } | nullimport { usePresence } from '@smarthivelabs-devs/hive-socket-react';
function UserAvatar({ userId, avatarUrl }: { userId: string; avatarUrl: string }) {
const presence = usePresence(userId);
const isOnline = presence?.status === 'online';
return (
<div style={{ position: 'relative', display: 'inline-block' }}>
<img src={avatarUrl} style={{ borderRadius: '50%', width: 40, height: 40 }} />
<span
style={{
position: 'absolute',
bottom: 0,
right: 0,
width: 10,
height: 10,
borderRadius: '50%',
backgroundColor: isOnline ? '#22c55e' : '#9ca3af',
border: '2px solid white',
}}
/>
</div>
);
}Update your own presence status:
const { updatePresence } = useHiveSocket();
// When user goes away (e.g. tab visibility change)
document.addEventListener('visibilitychange', () => {
updatePresence(document.hidden ? 'away' : 'online');
});useNotifications
Receive real-time notifications pushed from your backends. Pending (unread) notifications are loaded automatically on connect.
const {
notifications, // HiveNotification[]
unreadCount, // number
markRead, // (notificationId: string) => void
} = useNotifications();import { useNotifications } from '@smarthivelabs-devs/hive-socket-react';
function NotificationPanel() {
const { notifications, unreadCount, markRead } = useNotifications();
return (
<div>
<h3>Notifications {unreadCount > 0 && <span>({unreadCount})</span>}</h3>
{notifications.map((n) => (
<div
key={n.id}
onClick={() => !n.read && markRead(n.id)}
style={{ opacity: n.read ? 0.5 : 1, cursor: n.read ? 'default' : 'pointer' }}
>
<p style={{ fontWeight: n.read ? 'normal' : 'bold' }}>{n.title}</p>
<p>{n.body}</p>
<time>{new Date(n.createdAt).toLocaleString()}</time>
</div>
))}
{notifications.length === 0 && <p>No notifications</p>}
</div>
);
}Notification bell with badge:
function NotificationBell() {
const { unreadCount } = useNotifications();
return (
<div style={{ position: 'relative', display: 'inline-block' }}>
<span>🔔</span>
{unreadCount > 0 && (
<span
style={{
position: 'absolute',
top: -4,
right: -4,
background: 'red',
color: 'white',
borderRadius: '50%',
width: 18,
height: 18,
fontSize: 11,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</div>
);
}HiveNotification shape:
interface HiveNotification {
id: string;
title: string;
body: string;
type: 'info' | 'success' | 'warning' | 'error' | 'message';
metadata: Record<string, unknown>;
sourceService?: string; // which backend sent it
read: boolean;
readAt: string | null; // ISO date string
createdAt: string; // ISO date string
}useWebRTC
Peer-to-peer audio, video, screen sharing, and data channels over WebRTC. The existing Socket.IO connection is used for signaling (offer/answer/ICE) — the actual media streams flow directly between clients. No extra dependencies needed.
const {
callState, // 'idle' | 'calling' | 'incoming' | 'connecting' | 'active' | 'ended'
currentCall, // HiveWebRtcCall | null
localStream, // MediaStream | null — your camera/mic
remoteStream, // MediaStream | null — the other peer's stream
screenStream, // MediaStream | null — your screen share
dataChannel, // RTCDataChannel | null
isAudioMuted, // boolean
isVideoOff, // boolean
isSharingScreen, // boolean
startCall, // (targetUserId: string, callType: WebRtcCallType) => Promise<void>
acceptCall, // () => Promise<void>
rejectCall, // () => void
endCall, // () => void
toggleAudio, // () => void
toggleVideo, // () => void
startScreenShare, // () => Promise<void>
stopScreenShare, // () => void
sendData, // (data: string | ArrayBuffer) => void
} = useWebRTC();WebRtcCallType: 'audio' | 'video' | 'screen' | 'data'
Video call example:
import { useWebRTC } from '@smarthivelabs-devs/hive-socket-react';
import { useRef, useEffect } from 'react';
function VideoCall({ targetUserId }: { targetUserId: string }) {
const {
callState,
localStream,
remoteStream,
startCall,
acceptCall,
rejectCall,
endCall,
toggleAudio,
toggleVideo,
isAudioMuted,
isVideoOff,
} = useWebRTC();
const localVideoRef = useRef<HTMLVideoElement>(null);
const remoteVideoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (localVideoRef.current && localStream) {
localVideoRef.current.srcObject = localStream;
}
}, [localStream]);
useEffect(() => {
if (remoteVideoRef.current && remoteStream) {
remoteVideoRef.current.srcObject = remoteStream;
}
}, [remoteStream]);
if (callState === 'incoming') {
return (
<div>
<p>Incoming call...</p>
<button onClick={acceptCall}>Accept</button>
<button onClick={rejectCall}>Decline</button>
</div>
);
}
return (
<div>
{callState === 'idle' && (
<button onClick={() => startCall(targetUserId, 'video')}>Start Video Call</button>
)}
{(callState === 'calling' || callState === 'connecting' || callState === 'active') && (
<div>
<video ref={remoteVideoRef} autoPlay playsInline style={{ width: '100%' }} />
<video ref={localVideoRef} autoPlay playsInline muted style={{ width: 120, position: 'absolute', bottom: 16, right: 16 }} />
<div>
<button onClick={toggleAudio}>{isAudioMuted ? 'Unmute' : 'Mute'}</button>
<button onClick={toggleVideo}>{isVideoOff ? 'Show Camera' : 'Hide Camera'}</button>
<button onClick={endCall}>End Call</button>
</div>
</div>
)}
</div>
);
}Audio-only call:
await startCall(targetUserId, 'audio');Screen share (add to an active call):
await startScreenShare(); // adds screen track to the existing RTCPeerConnection
stopScreenShare(); // removes the screen trackData channel (low-latency bidirectional data):
const { startCall, dataChannel, sendData } = useWebRTC();
// Start a data-only call
await startCall(targetUserId, 'data');
// Send data once the channel is open
useEffect(() => {
if (!dataChannel) return;
dataChannel.onopen = () => sendData('hello peer');
dataChannel.onmessage = (e) => console.log('received:', e.data);
}, [dataChannel]);ICE / STUN configuration:
The server automatically emits ICE server config on connect (webrtc:config). The hook picks it up automatically — no manual ICE configuration needed. To add a TURN server for production NAT traversal, set WEBRTC_TURN_URLS, WEBRTC_TURN_USERNAME, and WEBRTC_TURN_CREDENTIAL on the Hive-Socket server.
Reconnect and missed events
The provider automatically tracks the last received event ID. On reconnect, it emits reconnect:sync so the server replays any events you missed (up to 5 minutes). No action required — it's built in.
Complete example — Workspace project chat
// app/(authed)/projects/[id]/chat/page.tsx
'use client';
import {
useHiveSocket,
useMessages,
useNotifications,
} from '@smarthivelabs-devs/hive-socket-react';
import { useState } from 'react';
export default function ProjectChatPage({ params }: { params: { id: string } }) {
const roomId = `project:${params.id}:general`;
const { status, sendMessage } = useHiveSocket();
const messages = useMessages(roomId);
const { unreadCount } = useNotifications();
const [text, setText] = useState('');
const send = () => {
if (!text.trim() || status !== 'connected') return;
sendMessage(roomId, 'chat.message', { text });
setText('');
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<div style={{ padding: 12, borderBottom: '1px solid #eee', display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: status === 'connected' ? 'green' : 'gray' }}>
{status}
</span>
<span>🔔 {unreadCount}</span>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: 16 }}>
{messages.map((msg) => (
<div key={msg._eid} style={{ marginBottom: 8 }}>
<strong>{msg.senderId}</strong>: {msg.payload.text as string}
</div>
))}
</div>
<div style={{ display: 'flex', padding: 12, borderTop: '1px solid #eee' }}>
<input
style={{ flex: 1, marginRight: 8 }}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && send()}
placeholder="Message..."
/>
<button onClick={send} disabled={status !== 'connected'}>
Send
</button>
</div>
</div>
);
}Room naming
Rooms are scoped per project by the server — you only pass the room name, never the project ID.
| Room pattern | Use case |
|-------------|----------|
| project:{id}:general | Project-wide chat |
| project:{id}:thread:{threadId} | Specific thread |
| course:{courseId} | Course-level updates |
| event:{eventId} | Live voting session |
Each user is automatically in their own user:{userId} room on connect — notifications sent by the backend always arrive without needing an explicit joinRoom.
Types
import type {
HiveMessage,
HiveHistoryMessage,
HiveNotification,
HivePresenceChange,
HiveSocketError,
HiveSocketProviderProps,
NotificationType,
PresenceStatus,
ConnectionStatus,
UseHiveSocketReturn,
PresenceState,
UseNotificationsReturn,
// WebRTC
WebRtcCallType,
WebRtcCallState,
HiveWebRtcCall,
HiveIceServer,
HiveWebRtcConfig,
UseWebRTCReturn,
} from '@smarthivelabs-devs/hive-socket-react';| Type | Description |
|------|-------------|
| HiveMessage | Real-time message from a room |
| HiveHistoryMessage | Message row from history fetch |
| HiveNotification | User notification (real-time or pending) |
| HivePresenceChange | Presence status change event |
| HiveSocketError | Server error payload |
| ConnectionStatus | 'connecting' \| 'connected' \| 'disconnected' |
| PresenceStatus | 'online' \| 'away' \| 'offline' |
| NotificationType | 'info' \| 'success' \| 'warning' \| 'error' \| 'message' |
| WebRtcCallType | 'audio' \| 'video' \| 'screen' \| 'data' |
| WebRtcCallState | 'idle' \| 'calling' \| 'incoming' \| 'connecting' \| 'active' \| 'ended' |
| HiveWebRtcCall | Active call metadata (callId, callerId, targetId, callType) |
| HiveIceServer | ICE server config (urls, optional username/credential) |
| HiveWebRtcConfig | ICE config emitted by server on connect |
Security
How the token is sent and validated
When you pass token to HiveSocketProvider, the SDK sends it in the Socket.IO handshake auth object:
io(url, { auth: { token } })The server receives it during the connection handshake — before any events are processed. It verifies the JWT using your SmartHive Auth project secret (AUTH_SECRET). If verification fails the connection is immediately rejected with UNAUTHORIZED and no events are ever processed.
After a successful verify, the server extracts userId and projectId directly from the JWT claims. You never pass these yourself — they cannot be spoofed by the client.
What the server enforces
| Guarantee | How |
|-----------|-----|
| Token is valid and unexpired | JWT signature verified on every new connection |
| User can only receive their own notifications | Server auto-joins {projectId}::user:{userId} — derived from the token, not from client input |
| User can only join rooms in their project | Room joins are namespaced to the token's projectId server-side |
| Stale tokens are rejected on reconnect | Every reconnect re-runs the full handshake verification |
What you must do
- Pass the access token, not the refresh token. The access token is short-lived; the server will reject an expired one and disconnect cleanly.
- Reconnect after a token refresh. The provider does this automatically — every time
tokenchanges, it tears down the old socket and opens a new connection with the new token. - Never pass the token in the URL (e.g.
?token=...). The SDK uses theauthhandshake field so the token is not logged by proxies or stored in browser history. - Keep
HiveSocketProviderinside your auth boundary. Only render it when the user is authenticated and a valid token is available. Pass''or skip rendering entirely when the token is absent — the provider skips connecting when the token is falsy.
Token lifecycle example (Next.js)
// app/providers.tsx
'use client';
import { HiveSocketProvider } from '@smarthivelabs-devs/hive-socket-react';
import { useSession } from '@smarthivelabs-devs/auth-react'; // SmartHive Auth hook
export function Providers({ children }: { children: React.ReactNode }) {
const { accessToken } = useSession(); // null before auth, auto-refreshed by the SDK
// Provider skips connecting when token is falsy — safe to render unconditionally
return (
<HiveSocketProvider url="https://socket.smarthivelabs.dev" token={accessToken ?? ''}>
{children}
</HiveSocketProvider>
);
}When SmartHive Auth silently refreshes the access token, accessToken changes → provider automatically reconnects with the new token. No manual handling needed.
Troubleshooting
Provider must be a Client Component
Add 'use client' to the component that renders HiveSocketProvider. In Next.js App Router, create a providers.tsx wrapper — do not add 'use client' to layout.tsx.
Connection immediately fails with UNAUTHORIZED
- The token must be a valid JWT from SmartHive Auth
- Pass the raw token — no
Bearerprefix - The provider reconnects automatically when you pass a new token
CORS error in browser
Your frontend origin must be in CORS_ORIGINS on the Hive-Socket server. In development: http://localhost:3000.
Messages missing after page refresh
History is loaded from the database on every mount via useMessages. If you see an empty list, check that the connection status reaches 'connected' — the hook waits for connection before joining and fetching.
