React Integration
Build SMS interfaces and testing workflows with React hooks and components.
Quick Setup
Create a React app with SMS capabilities in minutes.
1. Install Dependencies
# Create a new React app npx create-react-app sms-react-app cd sms-react-app # Install additional dependencies npm install axios # Install sms-dev globally npm install -g @relay/sms-dev # Start sms-dev in the background sms-dev
Custom Hooks
useSMS Hook
Custom hook for sending SMS messages and managing state
// hooks/useSMS.js
import { useState, useCallback } from 'react';
import axios from 'axios';
const SMS_API_URL = 'http://localhost:4001/v1/messages';
export function useSMS() {
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const sendMessage = useCallback(async (to, from, body) => {
setLoading(true);
setError(null);
try {
const response = await axios.post(SMS_API_URL, {
to,
from,
body
});
const newMessage = {
id: response.data.id,
to,
from,
body,
timestamp: new Date().toISOString(),
status: 'sent'
};
setMessages(prev => [...prev, newMessage]);
return newMessage;
} catch (err) {
const errorMessage = err.response?.data?.error || 'Failed to send SMS';
setError(errorMessage);
throw new Error(errorMessage);
} finally {
setLoading(false);
}
}, []);
const clearMessages = useCallback(() => {
setMessages([]);
setError(null);
}, []);
const clearError = useCallback(() => {
setError(null);
}, []);
return {
messages,
loading,
error,
sendMessage,
clearMessages,
clearError
};
}useConversation Hook
Manage conversation flows and message history
// hooks/useConversation.js
import { useState, useEffect, useCallback } from 'react';
import { useSMS } from './useSMS';
export function useConversation(phoneNumber) {
const { sendMessage, loading, error } = useSMS();
const [conversation, setConversation] = useState([]);
const [typing, setTyping] = useState(false);
// Simulate incoming messages for demo
const simulateReply = useCallback((replyText, delay = 2000) => {
setTyping(true);
setTimeout(() => {
const reply = {
id: `sim_${Date.now()}`,
to: '+1987654321',
from: phoneNumber,
body: replyText,
timestamp: new Date().toISOString(),
direction: 'inbound'
};
setConversation(prev => [...prev, reply]);
setTyping(false);
}, delay);
}, [phoneNumber]);
const sendConversationMessage = useCallback(async (body) => {
try {
const message = await sendMessage(
phoneNumber,
'+1987654321',
body
);
const conversationMessage = {
...message,
direction: 'outbound'
};
setConversation(prev => [...prev, conversationMessage]);
// Auto-reply based on message content
if (body.toLowerCase().includes('hello')) {
simulateReply('Hi! How can I help you today?');
} else if (body.toLowerCase().includes('help')) {
simulateReply('Available commands: HELP, INFO, TIME, JOKE');
} else if (body.toLowerCase().includes('time')) {
simulateReply(`Current time: ${new Date().toLocaleTimeString()}`);
} else {
simulateReply('Thanks for your message! This is an automated response.');
}
return conversationMessage;
} catch (err) {
console.error('Failed to send message:', err);
throw err;
}
}, [phoneNumber, sendMessage, simulateReply]);
const clearConversation = useCallback(() => {
setConversation([]);
}, []);
return {
conversation,
loading,
error,
typing,
sendMessage: sendConversationMessage,
clearConversation
};
}React Components
SMS Form Component
Reusable form component for sending SMS messages
// components/SMSForm.jsx
import React, { useState } from 'react';
import { useSMS } from '../hooks/useSMS';
export function SMSForm({ onMessageSent }) {
const [to, setTo] = useState('');
const [from, setFrom] = useState('+1987654321');
const [body, setBody] = useState('');
const { sendMessage, loading, error } = useSMS();
const handleSubmit = async (e) => {
e.preventDefault();
if (!to || !from || !body) {
alert('Please fill in all fields');
return;
}
try {
const message = await sendMessage(to, from, body);
setTo('');
setBody('');
onMessageSent?.(message);
} catch (err) {
// Error is handled by the hook
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="to" className="block text-sm font-medium text-gray-700">
To (Phone Number)
</label>
<input
type="tel"
id="to"
value={to}
onChange={(e) => setTo(e.target.value)}
placeholder="+1234567890"
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading}
/>
</div>
<div>
<label htmlFor="from" className="block text-sm font-medium text-gray-700">
From (Your Number)
</label>
<input
type="tel"
id="from"
value={from}
onChange={(e) => setFrom(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading}
/>
</div>
<div>
<label htmlFor="body" className="block text-sm font-medium text-gray-700">
Message
</label>
<textarea
id="body"
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Enter your message here..."
rows={3}
maxLength={160}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading}
/>
<p className="mt-1 text-sm text-gray-500">
{body.length}/160 characters
</p>
</div>
{error && (
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded">
{error}
</div>
)}
<button
type="submit"
disabled={loading || !to || !from || !body}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Sending...' : 'Send SMS'}
</button>
</form>
);
}Conversation Component
Chat-like interface for testing conversation flows
// components/Conversation.jsx
import React, { useState, useRef, useEffect } from 'react';
import { useConversation } from '../hooks/useConversation';
export function Conversation({ phoneNumber = '+1234567890' }) {
const [newMessage, setNewMessage] = useState('');
const { conversation, loading, typing, sendMessage, clearConversation } = useConversation(phoneNumber);
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [conversation, typing]);
const handleSend = async (e) => {
e.preventDefault();
if (!newMessage.trim() || loading) return;
try {
await sendMessage(newMessage.trim());
setNewMessage('');
} catch (err) {
console.error('Failed to send message:', err);
}
};
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
};
return (
<div className="flex flex-col h-96 border border-gray-300 rounded-lg overflow-hidden">
{/* Header */}
<div className="bg-blue-600 text-white p-3 flex justify-between items-center">
<h3 className="font-semibold">Conversation with {phoneNumber}</h3>
<button
onClick={clearConversation}
className="text-xs bg-blue-700 hover:bg-blue-800 px-2 py-1 rounded"
>
Clear
</button>
</div>
{/* Messages */}
<div className="flex-1 p-4 overflow-y-auto bg-gray-50">
{conversation.length === 0 && (
<div className="text-center text-gray-500 py-8">
<p>No messages yet. Send a message to start the conversation!</p>
</div>
)}
{conversation.map((message) => (
<div
key={message.id}
className={`flex mb-4 ${
message.direction === 'outbound' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
message.direction === 'outbound'
? 'bg-blue-600 text-white rounded-br-none'
: 'bg-white border border-gray-200 text-gray-800 rounded-bl-none'
}`}
>
<p className="text-sm whitespace-pre-wrap">{message.body}</p>
<p className={`text-xs mt-1 ${
message.direction === 'outbound' ? 'text-blue-100' : 'text-gray-500'
}`}>
{formatTime(message.timestamp)}
</p>
</div>
</div>
))}
{typing && (
<div className="flex justify-start mb-4">
<div className="bg-white border border-gray-200 rounded-lg rounded-bl-none px-4 py-2 max-w-xs">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<form onSubmit={handleSend} className="p-3 border-t border-gray-200 bg-white">
<div className="flex space-x-2">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type a message..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading}
/>
<button
type="submit"
disabled={!newMessage.trim() || loading}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Send
</button>
</div>
</form>
</div>
);
}Complete App Example
Put it all together in your main App component
// App.js
import React, { useState } from 'react';
import { SMSForm } from './components/SMSForm';
import { Conversation } from './components/Conversation';
import './App.css';
function App() {
const [selectedPhone, setSelectedPhone] = useState('+1234567890');
const [messagesSent, setMessagesSent] = useState(0);
const handleMessageSent = (message) => {
console.log('Message sent:', message);
setMessagesSent(prev => prev + 1);
};
return (
<div className="App">
<header className="bg-blue-600 text-white p-6">
<h1 className="text-3xl font-bold">SMS Development Dashboard</h1>
<p className="mt-2 text-blue-100">
Test your SMS applications with sms-dev
</p>
</header>
<main className="container mx-auto px-4 py-8">
<div className="grid md:grid-cols-2 gap-8">
{/* SMS Form */}
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Send SMS</h2>
<SMSForm onMessageSent={handleMessageSent} />
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h3 className="text-lg font-semibold mb-2">Statistics</h3>
<p className="text-gray-600">Messages sent: {messagesSent}</p>
<p className="text-gray-600">
Virtual Phone:
<a
href="http://localhost:4000"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline ml-1"
>
http://localhost:4000
</a>
</p>
</div>
</div>
{/* Conversation */}
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow-md">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Test Conversation</h2>
<select
value={selectedPhone}
onChange={(e) => setSelectedPhone(e.target.value)}
className="px-3 py-1 border border-gray-300 rounded text-sm"
>
<option value="+1234567890">+1 (234) 567-890</option>
<option value="+1555123456">+1 (555) 123-456</option>
<option value="+1999888777">+1 (999) 888-777</option>
</select>
</div>
<Conversation phoneNumber={selectedPhone} />
</div>
</div>
</div>
</main>
</div>
);
}
export default App;Running the Example
Step-by-Step Instructions
1
Start sms-dev
Run sms-dev in your terminal
2
Start your React app
Run npm start
3
Test the interface
Use the forms to send messages and see them in the Virtual Phone