useWayfinderUrl

Overview

The useWayfinderUrl hook resolves ar:// URLs to gateway URLs with built-in loading states and error handling. This hook is perfect for creating links, displaying resolved URLs, or managing URL resolution state in your React components.

Signature

function useWayfinderUrl({ txId }: { txId: string }): {
  resolvedUrl: string | null
  isLoading: boolean
  error: Error | null
  txId: string
}

Usage

Basic URL Resolution

import { useWayfinderUrl } from '@ar.io/wayfinder-react'

function UrlDisplay({ txId }) {
  const { resolvedUrl, isLoading, error } = useWayfinderUrl({ txId })

  if (isLoading) return <div>Resolving URL...</div>
  if (error) return <div>Error: {error.message}</div>
  if (!resolvedUrl) return <div>No URL to resolve</div>

  return (
    <div>
      <p>Transaction ID: {txId}</p>
      <p>
        Resolved URL:{' '}
        <a href={resolvedUrl} target="_blank" rel="noopener noreferrer">
          {resolvedUrl}
        </a>
      </p>
    </div>
  )
}

Creating Dynamic Links

import { useWayfinderUrl } from '@ar.io/wayfinder-react'

function ArweaveLink({ txId, children, className, ...props }) {
  const { resolvedUrl, isLoading, error } = useWayfinderUrl({ txId })

  if (isLoading) {
    return (
      <span className={`${className} loading`} {...props}>
        {children} (resolving...)
      </span>
    )
  }

  if (error) {
    return (
      <span className={`${className} error`} {...props}>
        {children} (error)
      </span>
    )
  }

  if (!resolvedUrl) {
    return (
      <span className={`${className} disabled`} {...props}>
        {children}
      </span>
    )
  }

  return (
    <a
      href={resolvedUrl}
      className={className}
      target="_blank"
      rel="noopener noreferrer"
      {...props}
    >
      {children}
    </a>
  )
}

// Usage
function MyComponent() {
  return (
    <div>
      <ArweaveLink txId="your-transaction-id" className="btn btn-primary">
        View on Arweave
      </ArweaveLink>
    </div>
  )
}

Image Gallery with URL Resolution

import { useWayfinderUrl } from '@ar.io/wayfinder-react'

function ArweaveImage({ txId, alt, className, ...props }) {
  const { resolvedUrl, isLoading, error } = useWayfinderUrl({ txId })

  if (isLoading) {
    return (
      <div className={`${className} loading-placeholder`} {...props}>
        <div className="spinner">Loading image...</div>
      </div>
    )
  }

  if (error) {
    return (
      <div className={`${className} error-placeholder`} {...props}>
        <div className="error-message">Failed to load image</div>
      </div>
    )
  }

  if (!resolvedUrl) {
    return (
      <div className={`${className} empty-placeholder`} {...props}>
        <div className="empty-message">No image to display</div>
      </div>
    )
  }

  return <img src={resolvedUrl} alt={alt} className={className} {...props} />
}

function ImageGallery({ imageIds }) {
  return (
    <div className="image-gallery">
      {imageIds.map((txId) => (
        <ArweaveImage
          key={txId}
          txId={txId}
          alt={`Arweave image ${txId}`}
          className="gallery-image"
        />
      ))}
    </div>
  )
}

Conditional Rendering Based on URL State

import { useWayfinderUrl } from '@ar.io/wayfinder-react'

function ConditionalContent({ txId }) {
  const {
    resolvedUrl,
    isLoading,
    error,
    txId: currentTxId,
  } = useWayfinderUrl({ txId })

  return (
    <div className="content-container">
      <div className="header">
        <h3>Content: {currentTxId}</h3>
        <div className="status">
          {isLoading && <span className="badge loading">Resolving</span>}
          {error && <span className="badge error">Error</span>}
          {resolvedUrl && <span className="badge success">Ready</span>}
        </div>
      </div>

      {isLoading && (
        <div className="loading-state">
          <div className="skeleton-loader"></div>
          <p>Resolving Arweave URL...</p>
        </div>
      )}

      {error && (
        <div className="error-state">
          <div className="error-icon">⚠️</div>
          <h4>Failed to resolve URL</h4>
          <p>{error.message}</p>
          <button onClick={() => window.location.reload()}>Try Again</button>
        </div>
      )}

      {resolvedUrl && (
        <div className="success-state">
          <div className="actions">
            <a
              href={resolvedUrl}
              target="_blank"
              rel="noopener noreferrer"
              className="btn btn-primary"
            >
              Open in New Tab
            </a>
            <button
              onClick={() => navigator.clipboard.writeText(resolvedUrl)}
              className="btn btn-secondary"
            >
              Copy URL
            </button>
          </div>

          <div className="url-preview">
            <label>Resolved URL:</label>
            <code>{resolvedUrl}</code>
          </div>
        </div>
      )}
    </div>
  )
}

Multiple URL Resolution

Bulk URL Resolution

import { useWayfinderUrl } from '@ar.io/wayfinder-react'

function MultipleUrlResolver({ txIds }) {
  return (
    <div className="url-list">
      <h3>Resolving {txIds.length} URLs</h3>
      {txIds.map((txId) => (
        <UrlItem key={txId} txId={txId} />
      ))}
    </div>
  )
}

function UrlItem({ txId }) {
  const { resolvedUrl, isLoading, error } = useWayfinderUrl({ txId })

  return (
    <div className="url-item">
      <div className="tx-id">
        <strong>TX:</strong> {txId}
      </div>

      <div className="resolution-status">
        {isLoading && <span className="status loading">Resolving...</span>}
        {error && <span className="status error">Error: {error.message}</span>}
        {resolvedUrl && (
          <div className="resolved">
            <span className="status success">✓ Resolved</span>
            <a href={resolvedUrl} target="_blank" rel="noopener noreferrer">
              {resolvedUrl}
            </a>
          </div>
        )}
      </div>
    </div>
  )
}

Summary Statistics

import { useWayfinderUrl } from '@ar.io/wayfinder-react'
import { useMemo } from 'react'

function UrlResolutionSummary({ txIds }) {
  const urlStates = txIds.map((txId) => {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    const state = useWayfinderUrl({ txId })
    return { txId, ...state }
  })

  const summary = useMemo(() => {
    const total = urlStates.length
    const loading = urlStates.filter((state) => state.isLoading).length
    const errors = urlStates.filter((state) => state.error).length
    const resolved = urlStates.filter((state) => state.resolvedUrl).length

    return { total, loading, errors, resolved }
  }, [urlStates])

  return (
    <div className="resolution-summary">
      <h3>URL Resolution Summary</h3>
      <div className="stats">
        <div className="stat">
          <span className="label">Total:</span>
          <span className="value">{summary.total}</span>
        </div>
        <div className="stat">
          <span className="label">Resolved:</span>
          <span className="value success">{summary.resolved}</span>
        </div>
        <div className="stat">
          <span className="label">Loading:</span>
          <span className="value loading">{summary.loading}</span>
        </div>
        <div className="stat">
          <span className="label">Errors:</span>
          <span className="value error">{summary.errors}</span>
        </div>
      </div>

      <div className="progress-bar">
        <div
          className="progress-fill"
          style={{
            width: `${(summary.resolved / summary.total) * 100}%`,
          }}
        />
      </div>
    </div>
  )
}

Custom Hooks

URL Resolution with Caching

import { useWayfinderUrl } from '@ar.io/wayfinder-react'
import { useState, useEffect, useCallback } from 'react'

// Simple cache implementation
const urlCache = new Map()

function useCachedWayfinderUrl(txId) {
  const [cachedUrl, setCachedUrl] = useState(urlCache.get(txId) || null)
  const { resolvedUrl, isLoading, error } = useWayfinderUrl({
    txId: cachedUrl ? null : txId, // Skip resolution if cached
  })

  useEffect(() => {
    if (resolvedUrl && !urlCache.has(txId)) {
      urlCache.set(txId, resolvedUrl)
      setCachedUrl(resolvedUrl)
    }
  }, [resolvedUrl, txId])

  const clearCache = useCallback(() => {
    urlCache.delete(txId)
    setCachedUrl(null)
  }, [txId])

  return {
    resolvedUrl: cachedUrl || resolvedUrl,
    isLoading: cachedUrl ? false : isLoading,
    error: cachedUrl ? null : error,
    isCached: !!cachedUrl,
    clearCache,
  }
}

// Usage
function CachedUrlDisplay({ txId }) {
  const { resolvedUrl, isLoading, error, isCached, clearCache } =
    useCachedWayfinderUrl(txId)

  return (
    <div>
      {isLoading && <div>Resolving URL...</div>}
      {error && <div>Error: {error.message}</div>}
      {resolvedUrl && (
        <div>
          <a href={resolvedUrl} target="_blank" rel="noopener noreferrer">
            {resolvedUrl}
          </a>
          {isCached && (
            <div>
              <span className="cache-indicator">📋 Cached</span>
              <button onClick={clearCache}>Clear Cache</button>
            </div>
          )}
        </div>
      )}
    </div>
  )
}

URL Resolution with Retry Logic

import { useWayfinderUrl } from '@ar.io/wayfinder-react'
import { useState, useEffect, useCallback } from 'react'

function useWayfinderUrlWithRetry(txId, maxRetries = 3) {
  const [retryCount, setRetryCount] = useState(0)
  const [retryTxId, setRetryTxId] = useState(txId)

  const { resolvedUrl, isLoading, error } = useWayfinderUrl({ txId: retryTxId })

  const retry = useCallback(() => {
    if (retryCount < maxRetries) {
      setRetryCount((prev) => prev + 1)
      // Force re-resolution by changing the txId reference
      setRetryTxId(`${txId}?retry=${retryCount + 1}`)
    }
  }, [txId, retryCount, maxRetries])

  const reset = useCallback(() => {
    setRetryCount(0)
    setRetryTxId(txId)
  }, [txId])

  const canRetry = retryCount < maxRetries && error && !isLoading

  return {
    resolvedUrl,
    isLoading,
    error,
    retryCount,
    canRetry,
    retry,
    reset,
  }
}

// Usage
function RetryableUrlDisplay({ txId }) {
  const { resolvedUrl, isLoading, error, retryCount, canRetry, retry, reset } =
    useWayfinderUrlWithRetry(txId, 3)

  return (
    <div>
      {isLoading && (
        <div>
          Resolving URL...
          {retryCount > 0 && ` (Attempt ${retryCount + 1})`}
        </div>
      )}

      {error && (
        <div className="error-container">
          <div>Error: {error.message}</div>
          {retryCount > 0 && <div>Failed after {retryCount + 1} attempts</div>}

          <div className="error-actions">
            {canRetry && (
              <button onClick={retry}>
                Retry ({retryCount + 1}/{3})
              </button>
            )}
            <button onClick={reset}>Reset</button>
          </div>
        </div>
      )}

      {resolvedUrl && (
        <div>
          <a href={resolvedUrl} target="_blank" rel="noopener noreferrer">
            {resolvedUrl}
          </a>
          {retryCount > 0 && (
            <div className="success-after-retry">
              ✓ Resolved after {retryCount + 1} attempts
            </div>
          )}
        </div>
      )}
    </div>
  )
}

Error Handling

Comprehensive Error Display

import { useWayfinderUrl } from '@ar.io/wayfinder-react'

function RobustUrlDisplay({ txId }) {
  const { resolvedUrl, isLoading, error } = useWayfinderUrl({ txId })

  const getErrorType = (error) => {
    if (!error) return null

    if (error.name === 'TimeoutError') return 'timeout'
    if (error.name === 'NetworkError') return 'network'
    if (error.message.includes('404')) return 'not-found'
    if (error.message.includes('gateway')) return 'gateway'
    return 'unknown'
  }

  const getErrorMessage = (error) => {
    const type = getErrorType(error)

    switch (type) {
      case 'timeout':
        return 'URL resolution timed out. The gateway may be slow or unavailable.'
      case 'network':
        return 'Network error occurred. Please check your internet connection.'
      case 'not-found':
        return 'Transaction not found. Please verify the transaction ID.'
      case 'gateway':
        return 'Gateway error occurred. The selected gateway may be unavailable.'
      default:
        return `An error occurred: ${error.message}`
    }
  }

  const getErrorIcon = (error) => {
    const type = getErrorType(error)

    switch (type) {
      case 'timeout':
        return '⏱️'
      case 'network':
        return '🌐'
      case 'not-found':
        return '🔍'
      case 'gateway':
        return '🚪'
      default:
        return '⚠️'
    }
  }

  if (isLoading) {
    return (
      <div className="url-display loading">
        <div className="spinner"></div>
        <span>Resolving URL for {txId}...</span>
      </div>
    )
  }

  if (error) {
    return (
      <div className="url-display error">
        <div className="error-header">
          <span className="error-icon">{getErrorIcon(error)}</span>
          <span className="error-title">URL Resolution Failed</span>
        </div>
        <div className="error-message">{getErrorMessage(error)}</div>
        <div className="error-details">
          <details>
            <summary>Technical Details</summary>
            <pre>{error.stack || error.message}</pre>
          </details>
        </div>
      </div>
    )
  }

  if (!resolvedUrl) {
    return (
      <div className="url-display empty">
        <span>No URL to resolve</span>
      </div>
    )
  }

  return (
    <div className="url-display success">
      <div className="url-header">
        <span className="success-icon">✅</span>
        <span className="url-title">URL Resolved</span>
      </div>
      <div className="url-content">
        <a
          href={resolvedUrl}
          target="_blank"
          rel="noopener noreferrer"
          className="resolved-link"
        >
          {resolvedUrl}
        </a>
        <button
          onClick={() => navigator.clipboard.writeText(resolvedUrl)}
          className="copy-button"
          title="Copy URL"
        >
          📋
        </button>
      </div>
    </div>
  )
}

Performance Optimization

Conditional Resolution

import { useWayfinderUrl } from '@ar.io/wayfinder-react'
import { useState } from 'react'

function LazyUrlResolver({ txId, autoResolve = false }) {
  const [shouldResolve, setShouldResolve] = useState(autoResolve)

  const { resolvedUrl, isLoading, error } = useWayfinderUrl({
    txId: shouldResolve ? txId : null,
  })

  const handleResolve = () => {
    setShouldResolve(true)
  }

  if (!shouldResolve) {
    return (
      <div className="lazy-resolver">
        <div className="tx-info">
          <strong>Transaction:</strong> {txId}
        </div>
        <button onClick={handleResolve} className="resolve-button">
          Resolve URL
        </button>
      </div>
    )
  }

  return (
    <div className="resolved-url">
      {isLoading && <div>Resolving...</div>}
      {error && <div>Error: {error.message}</div>}
      {resolvedUrl && (
        <a href={resolvedUrl} target="_blank" rel="noopener noreferrer">
          {resolvedUrl}
        </a>
      )}
    </div>
  )
}

Intersection Observer for Lazy Loading

import { useWayfinderUrl } from '@ar.io/wayfinder-react'
import { useState, useEffect, useRef } from 'react'

function IntersectionUrlResolver({ txId, rootMargin = '100px' }) {
  const [isVisible, setIsVisible] = useState(false)
  const containerRef = useRef(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true)
          observer.disconnect()
        }
      },
      { rootMargin },
    )

    if (containerRef.current) {
      observer.observe(containerRef.current)
    }

    return () => observer.disconnect()
  }, [rootMargin])

  const { resolvedUrl, isLoading, error } = useWayfinderUrl({
    txId: isVisible ? txId : null,
  })

  return (
    <div ref={containerRef} className="intersection-resolver">
      {!isVisible && (
        <div className="placeholder">
          <div className="tx-id">TX: {txId}</div>
          <div className="status">Waiting to resolve...</div>
        </div>
      )}

      {isVisible && (
        <>
          {isLoading && <div>Resolving URL...</div>}
          {error && <div>Error: {error.message}</div>}
          {resolvedUrl && (
            <a href={resolvedUrl} target="_blank" rel="noopener noreferrer">
              {resolvedUrl}
            </a>
          )}
        </>
      )}
    </div>
  )
}

TypeScript Support

Typed Usage

import { useWayfinderUrl } from '@ar.io/wayfinder-react'
import { useEffect } from 'react'

interface UrlDisplayProps {
  txId: string
  className?: string
  onResolve?: (url: string) => void
  onError?: (error: Error) => void
}

const TypedUrlDisplay: React.FC<UrlDisplayProps> = ({
  txId,
  className = '',
  onResolve,
  onError,
}) => {
  const { resolvedUrl, isLoading, error } = useWayfinderUrl({ txId })

  useEffect(() => {
    if (resolvedUrl && onResolve) {
      onResolve(resolvedUrl)
    }
  }, [resolvedUrl, onResolve])

  useEffect(() => {
    if (error && onError) {
      onError(error)
    }
  }, [error, onError])

  if (isLoading) {
    return <div className={`${className} loading`}>Resolving...</div>
  }

  if (error) {
    return <div className={`${className} error`}>Error: {error.message}</div>
  }

  if (!resolvedUrl) {
    return <div className={`${className} empty`}>No URL</div>
  }

  return (
    <a
      href={resolvedUrl}
      className={`${className} resolved`}
      target="_blank"
      rel="noopener noreferrer"
    >
      {resolvedUrl}
    </a>
  )
}

Custom Hook with TypeScript

import { useWayfinderUrl } from '@ar.io/wayfinder-react'
import { useMemo } from 'react'

interface UrlState {
  url: string | null
  loading: boolean
  error: Error | null
  status: 'idle' | 'loading' | 'success' | 'error'
}

function useUrlState(txId: string): UrlState {
  const { resolvedUrl, isLoading, error } = useWayfinderUrl({ txId })

  const state = useMemo((): UrlState => {
    if (isLoading) {
      return {
        url: null,
        loading: true,
        error: null,
        status: 'loading',
      }
    }

    if (error) {
      return {
        url: null,
        loading: false,
        error,
        status: 'error',
      }
    }

    if (resolvedUrl) {
      return {
        url: resolvedUrl,
        loading: false,
        error: null,
        status: 'success',
      }
    }

    return {
      url: null,
      loading: false,
      error: null,
      status: 'idle',
    }
  }, [resolvedUrl, isLoading, error])

  return state
}

// Usage
function StatusBasedComponent({ txId }: { txId: string }) {
  const { url, status, error } = useUrlState(txId)

  switch (status) {
    case 'loading':
      return <div className="loading">Resolving URL...</div>
    case 'error':
      return <div className="error">Error: {error?.message}</div>
    case 'success':
      return (
        <a href={url!} target="_blank" rel="noopener noreferrer">
          {url}
        </a>
      )
    default:
      return <div className="idle">Ready to resolve</div>
  }
}

Testing

Mocking the Hook

import { render, screen } from '@testing-library/react'
import { useWayfinderUrl } from '@ar.io/wayfinder-react'

// Mock the hook
jest.mock('@ar.io/wayfinder-react', () => ({
  useWayfinderUrl: jest.fn(),
}))

const mockUseWayfinderUrl = useWayfinderUrl as jest.MockedFunction<
  typeof useWayfinderUrl
>

describe('UrlDisplay', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })

  test('displays loading state', () => {
    mockUseWayfinderUrl.mockReturnValue({
      resolvedUrl: null,
      isLoading: true,
      error: null,
      txId: 'test-tx-id',
    })

    render(<UrlDisplay txId="test-tx-id" />)

    expect(screen.getByText('Resolving URL...')).toBeInTheDocument()
  })

  test('displays resolved URL', () => {
    mockUseWayfinderUrl.mockReturnValue({
      resolvedUrl: 'https://arweave.net/test-tx-id',
      isLoading: false,
      error: null,
      txId: 'test-tx-id',
    })

    render(<UrlDisplay txId="test-tx-id" />)

    const link = screen.getByRole('link')
    expect(link).toHaveAttribute('href', 'https://arweave.net/test-tx-id')
  })

  test('displays error state', () => {
    mockUseWayfinderUrl.mockReturnValue({
      resolvedUrl: null,
      isLoading: false,
      error: new Error('Resolution failed'),
      txId: 'test-tx-id',
    })

    render(<UrlDisplay txId="test-tx-id" />)

    expect(screen.getByText('Error: Resolution failed')).toBeInTheDocument()
  })
})

When to Use

Use useWayfinderUrl when you need:

  • URL resolution with state management: Built-in loading and error states
  • Creating links: Generate clickable links to Arweave content
  • URL display: Show resolved gateway URLs to users
  • Conditional rendering: Render different UI based on resolution state
  • Simple URL operations: Basic URL resolution without data fetching

For other use cases, consider:

Was this page helpful?