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:
- useWayfinderRequest for data fetching and processing
- useWayfinder for full Wayfinder API access and advanced features