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.

Performance Optimized

The hook automatically memoizes parameters to avoid unnecessary rerenders and improve performance. This optimization was added in version 1.0.6.

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?