The useJoinUrl hook returns the URL that other users can use to join the current React Together session. If there is no active session, this hook returns null.

This hook is essential for sharing collaborative sessions and inviting others to join your workspace.

Basic Usage

import { useJoinUrl } from 'react-together'

function ShareSession() {
  const joinUrl = useJoinUrl()

  if (!joinUrl) {
    return <p>Not connected to any session</p>
  }

  return (
    <div>
      <p>Share this URL with others:</p>
      <code>{joinUrl}</code>
    </div>
  )
}

Signature

useJoinUrl(): string | null

Return Value

joinUrl
string | null

The join URL for the current session, or null if not connected to any session.

Examples

Simple Share Button

Create a basic share functionality with copy-to-clipboard:

import { useJoinUrl } from 'react-together'
import { useState } from 'react'

export default function SimpleShareButton() {
  const joinUrl = useJoinUrl()
  const [copied, setCopied] = useState(false)

  const copyToClipboard = async () => {
    if (!joinUrl) return

    try {
      await navigator.clipboard.writeText(joinUrl)
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    } catch (err) {
      console.error('Failed to copy:', err)
    }
  }

  if (!joinUrl) {
    return (
      <div className="p-4 bg-gray-100 rounded-lg text-center">
        <p className="text-gray-600">Not connected to any session</p>
      </div>
    )
  }

  return (
    <div className="p-4 bg-white rounded-lg shadow">
      <h3 className="text-lg font-semibold mb-3">Invite Others</h3>
      
      <div className="flex items-center space-x-2 mb-3">
        <input
          type="text"
          value={joinUrl}
          readOnly
          className="flex-1 px-3 py-2 bg-gray-50 border border-gray-300 rounded text-sm"
        />
        <button
          onClick={copyToClipboard}
          className={`px-4 py-2 rounded font-medium transition-all ${
            copied 
              ? 'bg-green-500 text-white' 
              : 'bg-blue-500 hover:bg-blue-600 text-white'
          }`}
        >
          {copied ? '✓ Copied' : 'Copy'}
        </button>
      </div>
      
      <p className="text-sm text-gray-600">
        Share this link for others to join your session
      </p>
    </div>
  )
}

Advanced Share Dialog

Create a comprehensive sharing interface with multiple sharing options:

import { useJoinUrl, useConnectedUsers } from 'react-together'
import { useState } from 'react'

export default function ShareDialog() {
  const joinUrl = useJoinUrl()
  const connectedUsers = useConnectedUsers()
  const [isOpen, setIsOpen] = useState(false)
  const [copied, setCopied] = useState(false)

  const copyToClipboard = async () => {
    if (!joinUrl) return

    try {
      await navigator.clipboard.writeText(joinUrl)
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    } catch (err) {
      console.error('Failed to copy:', err)
    }
  }

  const shareViaEmail = () => {
    if (!joinUrl) return
    
    const subject = encodeURIComponent('Join my collaborative session')
    const body = encodeURIComponent(
      `Hi!\n\nI'm inviting you to join my collaborative session. Click the link below to get started:\n\n${joinUrl}\n\nSee you there!`
    )
    
    window.open(`mailto:?subject=${subject}&body=${body}`)
  }

  const shareViaText = () => {
    if (!joinUrl) return
    
    const message = encodeURIComponent(
      `Join my collaborative session: ${joinUrl}`
    )
    
    window.open(`sms:?body=${message}`)
  }

  if (!joinUrl) {
    return null
  }

  return (
    <div className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all"
      >
        👥 Invite Others ({connectedUsers.length} online)
      </button>

      {isOpen && (
        <div className="absolute top-full mt-2 right-0 bg-white rounded-lg shadow-lg border p-6 w-96 z-10">
          <div className="flex justify-between items-center mb-4">
            <h3 className="text-lg font-semibold">Share Session</h3>
            <button
              onClick={() => setIsOpen(false)}
              className="text-gray-400 hover:text-gray-600"
            >

            </button>
          </div>

          {/* Session URL */}
          <div className="mb-4">
            <label className="block text-sm font-medium text-gray-700 mb-2">
              Session URL
            </label>
            <div className="flex items-center space-x-2">
              <input
                type="text"
                value={joinUrl}
                readOnly
                className="flex-1 px-3 py-2 bg-gray-50 border border-gray-300 rounded text-sm font-mono"
              />
              <button
                onClick={copyToClipboard}
                className={`px-3 py-2 rounded text-sm font-medium transition-all ${
                  copied 
                    ? 'bg-green-500 text-white' 
                    : 'bg-gray-500 hover:bg-gray-600 text-white'
                }`}
              >
                {copied ? '✓' : '📋'}
              </button>
            </div>
          </div>

          {/* Share Options */}
          <div className="mb-4">
            <label className="block text-sm font-medium text-gray-700 mb-2">
              Share via
            </label>
            <div className="grid grid-cols-2 gap-2">
              <button
                onClick={shareViaEmail}
                className="flex items-center justify-center space-x-2 px-3 py-2 bg-blue-50 text-blue-700 rounded hover:bg-blue-100 transition-all"
              >
                <span>📧</span>
                <span>Email</span>
              </button>
              <button
                onClick={shareViaText}
                className="flex items-center justify-center space-x-2 px-3 py-2 bg-green-50 text-green-700 rounded hover:bg-green-100 transition-all"
              >
                <span>💬</span>
                <span>SMS</span>
              </button>
            </div>
          </div>

          {/* Current Participants */}
          <div className="border-t pt-4">
            <h4 className="text-sm font-medium text-gray-700 mb-2">
              Current Participants ({connectedUsers.length})
            </h4>
            <div className="space-y-1">
              {connectedUsers.slice(0, 3).map(({ userId, nickname, isYou }) => (
                <div key={userId} className="flex items-center space-x-2 text-sm">
                  <div className="w-2 h-2 bg-green-500 rounded-full"></div>
                  <span>
                    {nickname || `User ${userId.slice(-4)}`}
                    {isYou && ' (You)'}
                  </span>
                </div>
              ))}
              {connectedUsers.length > 3 && (
                <div className="text-xs text-gray-500">
                  +{connectedUsers.length - 3} more participants
                </div>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

QR Code Share

Generate a QR code for easy mobile sharing:

import { useJoinUrl } from 'react-together'
import { useState, useEffect } from 'react'

// Note: You'll need to install a QR code library like 'qrcode'
// npm install qrcode @types/qrcode

export default function QRCodeShare() {
  const joinUrl = useJoinUrl()
  const [qrCodeUrl, setQrCodeUrl] = useState<string>('')
  const [showQR, setShowQR] = useState(false)

  useEffect(() => {
    if (joinUrl) {
      // Generate QR code (you'll need to install qrcode library)
      // import QRCode from 'qrcode'
      // QRCode.toDataURL(joinUrl).then(setQrCodeUrl)
      
      // For demo purposes, using a placeholder QR service
      const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(joinUrl)}`
      setQrCodeUrl(qrUrl)
    }
  }, [joinUrl])

  if (!joinUrl) {
    return (
      <div className="p-4 bg-gray-100 rounded-lg text-center">
        <p className="text-gray-600">No active session to share</p>
      </div>
    )
  }

  return (
    <div className="p-6 bg-white rounded-lg shadow-lg max-w-sm mx-auto">
      <h3 className="text-lg font-semibold mb-4 text-center">Share Session</h3>
      
      {!showQR ? (
        <div className="space-y-4">
          <div className="text-center">
            <div className="text-6xl mb-2">📱</div>
            <p className="text-gray-600 text-sm">
              Share your session instantly with a QR code
            </p>
          </div>
          
          <button
            onClick={() => setShowQR(true)}
            className="w-full py-3 px-4 bg-blue-500 text-white font-semibold rounded-lg hover:bg-blue-600 transition-all"
          >
            Generate QR Code
          </button>
        </div>
      ) : (
        <div className="space-y-4">
          <div className="text-center">
            {qrCodeUrl && (
              <img
                src={qrCodeUrl}
                alt="Session QR Code"
                className="mx-auto mb-3 rounded"
              />
            )}
            <p className="text-sm text-gray-600 mb-3">
              Scan with your phone to join the session
            </p>
          </div>
          
          <div className="space-y-2">
            <button
              onClick={() => navigator.clipboard.writeText(joinUrl)}
              className="w-full py-2 px-4 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-all"
            >
              📋 Copy Link
            </button>
            
            <button
              onClick={() => setShowQR(false)}
              className="w-full py-2 px-4 border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-all"
            >
              ← Back
            </button>
          </div>
        </div>
      )}
    </div>
  )
}

Session Info Panel

Display comprehensive session information including join URL:

import { useJoinUrl, useConnectedUsers, useIsTogether } from 'react-together'
import { useState, useEffect } from 'react'

export default function SessionInfoPanel() {
  const joinUrl = useJoinUrl()
  const connectedUsers = useConnectedUsers()
  const isTogether = useIsTogether()
  const [sessionDuration, setSessionDuration] = useState(0)
  const [startTime] = useState(Date.now())

  useEffect(() => {
    if (!isTogether) return

    const interval = setInterval(() => {
      setSessionDuration(Date.now() - startTime)
    }, 1000)

    return () => clearInterval(interval)
  }, [isTogether, startTime])

  const formatDuration = (ms: number) => {
    const seconds = Math.floor(ms / 1000) % 60
    const minutes = Math.floor(ms / 60000) % 60
    const hours = Math.floor(ms / 3600000)
    
    if (hours > 0) {
      return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
    }
    return `${minutes}:${seconds.toString().padStart(2, '0')}`
  }

  const getSessionId = () => {
    if (!joinUrl) return 'N/A'
    try {
      const url = new URL(joinUrl)
      const name = url.searchParams.get('name')
      return name ? name.slice(-8).toUpperCase() : 'Unknown'
    } catch {
      return 'Unknown'
    }
  }

  const shareSession = async () => {
    if (!joinUrl) return

    try {
      await navigator.share({
        title: 'Join my collaborative session',
        text: 'Join my React Together session',
        url: joinUrl
      })
    } catch (err) {
      // Fallback to clipboard
      navigator.clipboard.writeText(joinUrl)
      alert('Session URL copied to clipboard!')
    }
  }

  if (!isTogether || !joinUrl) {
    return (
      <div className="p-4 bg-gray-100 rounded-lg">
        <div className="text-center text-gray-600">
          <div className="text-2xl mb-2">🚫</div>
          <p>No active session</p>
        </div>
      </div>
    )
  }

  return (
    <div className="p-6 bg-white rounded-lg shadow-lg">
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-lg font-semibold">Session Info</h3>
        <div className="flex items-center space-x-2">
          <div className="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
          <span className="text-green-600 font-medium text-sm">Active</span>
        </div>
      </div>

      <div className="space-y-4">
        {/* Session Statistics */}
        <div className="grid grid-cols-3 gap-4 p-4 bg-gray-50 rounded-lg">
          <div className="text-center">
            <div className="text-2xl font-bold text-blue-600">{connectedUsers.length}</div>
            <div className="text-xs text-gray-600">Participants</div>
          </div>
          <div className="text-center">
            <div className="text-2xl font-bold text-green-600">{formatDuration(sessionDuration)}</div>
            <div className="text-xs text-gray-600">Duration</div>
          </div>
          <div className="text-center">
            <div className="text-2xl font-bold text-purple-600">{getSessionId()}</div>
            <div className="text-xs text-gray-600">Session ID</div>
          </div>
        </div>

        {/* Participants List */}
        <div>
          <h4 className="font-medium text-gray-900 mb-2">Participants</h4>
          <div className="space-y-1">
            {connectedUsers.map(({ userId, nickname, isYou }) => (
              <div key={userId} className="flex items-center space-x-2 py-1">
                <div className="w-2 h-2 bg-green-500 rounded-full"></div>
                <span className="text-sm">
                  {nickname || `User ${userId.slice(-4)}`}
                  {isYou && <span className="text-blue-600 font-medium"> (You)</span>}
                </span>
              </div>
            ))}
          </div>
        </div>

        {/* Share Section */}
        <div className="border-t pt-4">
          <div className="flex items-center justify-between mb-2">
            <h4 className="font-medium text-gray-900">Share Session</h4>
            <button
              onClick={shareSession}
              className="px-3 py-1 bg-blue-500 text-white text-sm rounded hover:bg-blue-600 transition-all"
            >
              Share
            </button>
          </div>
          
          <div className="p-2 bg-gray-50 rounded text-xs font-mono text-gray-600 break-all">
            {joinUrl}
          </div>
        </div>
      </div>
    </div>
  )
}

Invitation Manager

Create a sophisticated invitation system:

import { useJoinUrl, useConnectedUsers } from 'react-together'
import { useState } from 'react'

interface Invitation {
  id: string
  email: string
  sentAt: number
  status: 'sent' | 'joined' | 'expired'
}

export default function InvitationManager() {
  const joinUrl = useJoinUrl()
  const connectedUsers = useConnectedUsers()
  const [invitations, setInvitations] = useState<Invitation[]>([])
  const [email, setEmail] = useState('')
  const [isSending, setIsSending] = useState(false)

  const sendInvitation = async () => {
    if (!email || !joinUrl) return

    setIsSending(true)
    
    try {
      // Simulate sending email (replace with actual email service)
      await new Promise(resolve => setTimeout(resolve, 1000))
      
      const newInvitation: Invitation = {
        id: Date.now().toString(),
        email,
        sentAt: Date.now(),
        status: 'sent'
      }
      
      setInvitations(prev => [newInvitation, ...prev])
      setEmail('')
      
      // In a real app, you would send an actual email here
      console.log(`Invitation sent to ${email}: ${joinUrl}`)
      
    } catch (error) {
      console.error('Failed to send invitation:', error)
    } finally {
      setIsSending(false)
    }
  }

  const formatTime = (timestamp: number) => {
    return new Date(timestamp).toLocaleString()
  }

  if (!joinUrl) {
    return (
      <div className="p-6 bg-gray-100 rounded-lg text-center">
        <p className="text-gray-600">No active session to invite users to</p>
      </div>
    )
  }

  return (
    <div className="p-6 bg-white rounded-lg shadow-lg max-w-2xl mx-auto">
      <h3 className="text-xl font-semibold mb-6">Invitation Manager</h3>
      
      {/* Send Invitation */}
      <div className="mb-6 p-4 bg-blue-50 rounded-lg">
        <h4 className="font-medium text-blue-900 mb-3">Send Invitation</h4>
        <div className="flex space-x-2">
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="Enter email address"
            className="flex-1 px-3 py-2 border border-blue-200 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
          />
          <button
            onClick={sendInvitation}
            disabled={!email || isSending}
            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
          >
            {isSending ? 'Sending...' : 'Send'}
          </button>
        </div>
      </div>

      {/* Current Session */}
      <div className="mb-6">
        <h4 className="font-medium text-gray-900 mb-3">Current Session</h4>
        <div className="grid grid-cols-2 gap-4 p-4 bg-green-50 rounded-lg">
          <div>
            <span className="text-sm text-green-700">Participants Online:</span>
            <div className="font-semibold text-green-900">{connectedUsers.length}</div>
          </div>
          <div>
            <span className="text-sm text-green-700">Session URL:</span>
            <div className="font-mono text-xs text-green-800 truncate">{joinUrl}</div>
          </div>
        </div>
      </div>

      {/* Invitation History */}
      <div>
        <h4 className="font-medium text-gray-900 mb-3">
          Invitation History ({invitations.length})
        </h4>
        
        {invitations.length === 0 ? (
          <div className="text-center py-8 text-gray-500">
            No invitations sent yet
          </div>
        ) : (
          <div className="space-y-2">
            {invitations.map((invitation) => (
              <div key={invitation.id} className="flex items-center justify-between p-3 bg-gray-50 rounded">
                <div>
                  <div className="font-medium">{invitation.email}</div>
                  <div className="text-sm text-gray-500">
                    Sent {formatTime(invitation.sentAt)}
                  </div>
                </div>
                <div>
                  <span className={`px-2 py-1 text-xs font-medium rounded ${
                    invitation.status === 'sent' ? 'bg-yellow-100 text-yellow-800' :
                    invitation.status === 'joined' ? 'bg-green-100 text-green-800' :
                    'bg-red-100 text-red-800'
                  }`}>
                    {invitation.status}
                  </span>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  )
}

Best Practices

URL Handling

  • Always check for null - The hook returns null when not in a session
  • Validate URLs - Ensure the URL is valid before sharing or processing
  • Handle URL parameters - Be aware of query parameters in the join URL

User Experience

  • Copy feedback - Provide clear feedback when URLs are copied
  • Share options - Offer multiple ways to share (copy, email, SMS, QR code)
  • Visual indicators - Show session status clearly

Security Considerations

  • URL sensitivity - Join URLs contain session credentials, handle them securely
  • Expiration awareness - URLs may become invalid when sessions end
  • Access control - Consider implementing additional access controls if needed

Common Patterns

Share Button with Copy

const shareSession = () => {
  if (joinUrl) {
    navigator.clipboard.writeText(joinUrl)
    alert('Session URL copied!')
  }
}

Email Invitation

const inviteViaEmail = (email: string) => {
  if (joinUrl) {
    const subject = 'Join my collaborative session'
    const body = `Join here: ${joinUrl}`
    window.open(`mailto:${email}?subject=${subject}&body=${body}`)
  }
}

URL Validation

const isValidJoinUrl = (url: string | null) => {
  if (!url) return false
  try {
    const parsed = new URL(url)
    return parsed.searchParams.has('name')
  } catch {
    return false
  }
}

TypeScript Support

This hook has simple, type-safe return value:

const joinUrl: string | null = useJoinUrl()

// Type guards for safe usage
if (joinUrl) {
  // joinUrl is guaranteed to be string here
  navigator.clipboard.writeText(joinUrl)
}

// Optional chaining is also safe
const urlLength = joinUrl?.length ?? 0