Examples/React

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