# Fetch API 🌐 SigPro provides a simple, lightweight wrapper around the native Fetch API that integrates seamlessly with signals for loading state management. It's designed for common use cases with sensible defaults. ## Core Concepts ### What is `$.fetch`? A ultra-simple fetch wrapper that: - **Automatically handles JSON** serialization and parsing - **Integrates with signals** for loading state - **Returns `null` on error** (no try/catch needed for basic usage) - **Works great with effects** for reactive data fetching ## `$.fetch(url, data, [loading])` Makes a POST request with JSON data and optional loading signal. ```javascript import { $ } from 'sigpro'; const loading = $(false); async function loadUser() { const user = await $.fetch('/api/user', { id: 123 }, loading); if (user) { console.log('User loaded:', user); } } ``` ## 📋 API Reference ### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `url` | `string` | Endpoint URL | | `data` | `Object` | Data to send (automatically JSON.stringify'd) | | `loading` | `Function` (optional) | Signal function to track loading state | ### Returns | Return | Description | |--------|-------------| | `Promise` | Parsed JSON response or `null` on error | ## 🎯 Basic Examples ### Simple Data Fetching ```javascript import { $ } from 'sigpro'; const userData = $(null); async function fetchUser(id) { const data = await $.fetch('/api/user', { id }); if (data) { userData(data); } } fetchUser(123); ``` ### With Loading State ```javascript import { $, html } from 'sigpro'; const user = $(null); const loading = $(false); async function loadUser(id) { const data = await $.fetch('/api/user', { id }, loading); if (data) user(data); } // In your template html`
${() => loading() ? html`
Loading...
` : user() ? html`

${user().name}

Email: ${user().email}

` : html`

No user found

`}
`; ``` ### In an Effect ```javascript import { $ } from 'sigpro'; const userId = $(1); const user = $(null); const loading = $(false); $.effect(() => { const id = userId(); if (id) { $.fetch(`/api/users/${id}`, null, loading).then(data => { if (data) user(data); }); } }); userId(2); // Automatically fetches new user ``` ## 🚀 Advanced Examples ### User Profile with Loading States ```javascript import { $, html } from 'sigpro'; const Profile = () => { const userId = $(1); const user = $(null); const loading = $(false); const error = $(null); const fetchUser = async (id) => { error(null); const data = await $.fetch('/api/user', { id }, loading); if (data) { user(data); } else { error('Failed to load user'); } }; // Fetch when userId changes $.effect(() => { fetchUser(userId()); }); return html`
${() => { if (loading()) { return html`
Loading profile...
`; } if (error()) { return html`
${error()}
`; } if (user()) { return html` `; } return html`

Select a user

`; }}
`; }; ``` ### Todo List with API ```javascript import { $, html } from 'sigpro'; const TodoApp = () => { const todos = $([]); const loading = $(false); const newTodo = $(''); const filter = $('all'); // 'all', 'active', 'completed' // Load todos const loadTodos = async () => { const data = await $.fetch('/api/todos', {}, loading); if (data) todos(data); }; // Add todo const addTodo = async () => { if (!newTodo().trim()) return; const todo = await $.fetch('/api/todos', { text: newTodo(), completed: false }); if (todo) { todos([...todos(), todo]); newTodo(''); } }; // Toggle todo const toggleTodo = async (id, completed) => { const updated = await $.fetch(`/api/todos/${id}`, { completed: !completed }); if (updated) { todos(todos().map(t => t.id === id ? updated : t )); } }; // Delete todo const deleteTodo = async (id) => { const result = await $.fetch(`/api/todos/${id}/delete`, {}); if (result) { todos(todos().filter(t => t.id !== id)); } }; // Filtered todos const filteredTodos = $(() => { const currentFilter = filter(); if (currentFilter === 'all') return todos(); if (currentFilter === 'active') { return todos().filter(t => !t.completed); } return todos().filter(t => t.completed); }); // Load on mount loadTodos(); return html`

Todo List

${() => loading() ? html`
Loading todos...
) : html` `}
`; }; ``` ### Infinite Scroll with Pagination ```javascript import { $, html } from 'sigpro'; const InfiniteScroll = () => { const posts = $([]); const page = $(1); const loading = $(false); const hasMore = $(true); const error = $(null); const loadMore = async () => { if (loading() || !hasMore()) return; const data = await $.fetch('/api/posts', { page: page(), limit: 10 }, loading); if (data) { if (data.posts.length === 0) { hasMore(false); } else { posts([...posts(), ...data.posts]); page(p => p + 1); } } else { error('Failed to load posts'); } }; // Intersection Observer for infinite scroll $.effect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { loadMore(); } }, { threshold: 0.1 } ); const sentinel = document.getElementById('sentinel'); if (sentinel) observer.observe(sentinel); return () => observer.disconnect(); }); // Initial load loadMore(); return html`

Posts

${posts().map(post => html`

${post.title}

${post.body}

By ${post.author}
`)}
${() => { if (loading()) { return html`
Loading more...
`; } if (error()) { return html`
${error()}
`; } if (!hasMore()) { return html`
No more posts
`; } return ''; }}
`; }; ``` ### Search with Debounce ```javascript import { $, html } from 'sigpro'; const SearchComponent = () => { const query = $(''); const results = $([]); const loading = $(false); const error = $(null); let searchTimeout; const performSearch = async (searchQuery) => { if (!searchQuery.trim()) { results([]); return; } const data = await $.fetch('/api/search', { q: searchQuery }, loading); if (data) { results(data); } else { error('Search failed'); } }; // Debounced search $.effect(() => { const searchQuery = query(); clearTimeout(searchTimeout); if (searchQuery.length < 2) { results([]); return; } searchTimeout = setTimeout(() => { performSearch(searchQuery); }, 300); return () => clearTimeout(searchTimeout); }); return html` `; }; ``` ### Form Submission ```javascript import { $, html } from 'sigpro'; const ContactForm = () => { const formData = $({ name: '', email: '', message: '' }); const submitting = $(false); const submitError = $(null); const submitSuccess = $(false); const handleSubmit = async (e) => { e.preventDefault(); submitError(null); submitSuccess(false); const result = await $.fetch('/api/contact', formData(), submitting); if (result) { submitSuccess(true); formData({ name: '', email: '', message: '' }); } else { submitError('Failed to send message. Please try again.'); } }; const updateField = (field, value) => { formData({ ...formData(), [field]: value }); }; return html`

Contact Us

formData().name} @input=${(e) => updateField('name', e.target.value)} required ?disabled=${submitting} />
formData().email} @input=${(e) => updateField('email', e.target.value)} required ?disabled=${submitting} />
${() => { if (submitting()) { return html`
Sending...
`; } if (submitError()) { return html`
${submitError()}
`; } if (submitSuccess()) { return html`
Message sent successfully!
`; } return ''; }}
`; }; ``` ### Real-time Dashboard with Multiple Endpoints ```javascript import { $, html } from 'sigpro'; const Dashboard = () => { // Multiple data streams const metrics = $({}); const alerts = $([]); const logs = $([]); const loading = $({ metrics: false, alerts: false, logs: false }); const refreshInterval = $(5000); // 5 seconds const fetchMetrics = async () => { const data = await $.fetch('/api/metrics', {}, loading().metrics); if (data) metrics(data); }; const fetchAlerts = async () => { const data = await $.fetch('/api/alerts', {}, loading().alerts); if (data) alerts(data); }; const fetchLogs = async () => { const data = await $.fetch('/api/logs', { limit: 50 }, loading().logs); if (data) logs(data); }; // Auto-refresh all data $.effect(() => { fetchMetrics(); fetchAlerts(); fetchLogs(); const interval = setInterval(() => { fetchMetrics(); fetchAlerts(); }, refreshInterval()); return () => clearInterval(interval); }); return html`

System Dashboard

System Metrics

${() => loading().metrics ? html`
Loading metrics...
) : html`
${metrics().cpu || 0}%
${metrics().memory || 0}%
${metrics().requests || 0}/s
`}

Active Alerts

${() => loading().alerts ? html`
Loading alerts...
) : alerts().length > 0 ? html`
    ${alerts().map(alert => html`
  • ${alert.type}

    ${alert.message}

    ${new Date(alert.timestamp).toLocaleTimeString()}
  • `)}
) : html`

No active alerts

`}

Recent Logs

${() => loading().logs ? html`
Loading logs...
) : html`
    ${logs().map(log => html`
  • ${new Date(log.timestamp).toLocaleTimeString()} ${log.message}
  • `)}
`}
`; }; ``` ### File Upload ```javascript import { $, html } from 'sigpro'; const FileUploader = () => { const files = $([]); const uploading = $(false); const uploadProgress = $({}); const uploadResults = $([]); const handleFileSelect = (e) => { files([...e.target.files]); }; const uploadFiles = async () => { if (files().length === 0) return; uploading(true); uploadResults([]); for (const file of files()) { const formData = new FormData(); formData.append('file', file); // Track progress for this file uploadProgress({ ...uploadProgress(), [file.name]: 0 }); try { // Custom fetch for FormData const response = await fetch('/api/upload', { method: 'POST', body: formData }); const result = await response.json(); uploadResults([ ...uploadResults(), { file: file.name, success: true, result } ]); } catch (error) { uploadResults([ ...uploadResults(), { file: file.name, success: false, error: error.message } ]); } uploadProgress({ ...uploadProgress(), [file.name]: 100 }); } uploading(false); }; return html`

Upload Files

${() => files().length > 0 ? html`

Selected Files:

    ${files().map(file => html`
  • ${file.name} (${(file.size / 1024).toFixed(2)} KB) ${() => uploadProgress()[file.name] ? html` ) : ''}
  • `)}
` : ''} ${() => uploadResults().length > 0 ? html`

Upload Results:

    ${uploadResults().map(result => html`
  • ${result.file}: ${result.success ? 'Uploaded successfully' : `Failed: ${result.error}`}
  • `)}
` : ''}
`; }; ``` ### Retry Logic ```javascript import { $ } from 'sigpro'; // Enhanced fetch with retry const fetchWithRetry = async (url, data, loading, maxRetries = 3) => { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { if (loading) loading(true); const result = await $.fetch(url, data); if (result !== null) { return result; } // If we get null but no error, wait and retry if (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000) // Exponential backoff ); } } catch (error) { lastError = error; console.warn(`Attempt ${attempt} failed:`, error); if (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000) ); } } finally { if (attempt === maxRetries && loading) { loading(false); } } } console.error('All retry attempts failed:', lastError); return null; }; // Usage const loading = $(false); const data = await fetchWithRetry('/api/unreliable-endpoint', {}, loading, 5); ``` ## 🎯 Best Practices ### 1. Always Handle Null Responses ```javascript // ❌ Don't assume success const data = await $.fetch('/api/data'); console.log(data.property); // Might throw if data is null // ✅ Check for null const data = await $.fetch('/api/data'); if (data) { console.log(data.property); } else { showError('Failed to load data'); } ``` ### 2. Use with Effects for Reactivity ```javascript // ❌ Manual fetching button.addEventListener('click', async () => { const data = await $.fetch('/api/data'); updateUI(data); }); // ✅ Reactive fetching const trigger = $(false); $.effect(() => { if (trigger()) { $.fetch('/api/data').then(data => { if (data) updateUI(data); }); } }); trigger(true); // Triggers fetch ``` ### 3. Combine with Loading Signals ```javascript // ✅ Always show loading state const loading = $(false); const data = $(null); async function load() { const result = await $.fetch('/api/data', {}, loading); if (result) data(result); } // In template html`
${() => loading() ? '' : data() ? '' : ''}
`; ``` ### 4. Cancel In-flight Requests ```javascript // ✅ Use AbortController with effects let controller; $.effect(() => { if (controller) { controller.abort(); } controller = new AbortController(); fetch(url, { signal: controller.signal }) .then(res => res.json()) .then(data => { if (!controller.signal.aborted) { updateData(data); } }); return () => controller.abort(); }); ``` ## 📊 Error Handling ### Basic Error Handling ```javascript const data = await $.fetch('/api/data'); if (!data) { // Handle error (show message, retry, etc.) } ``` ### With Error Signal ```javascript const data = $(null); const error = $(null); const loading = $(false); async function loadData() { error(null); const result = await $.fetch('/api/data', {}, loading); if (result) { data(result); } else { error('Failed to load data'); } } ``` --- > **Pro Tip:** Combine `$.fetch` with `$.effect` and loading signals for a complete reactive data fetching solution. The loading signal integration makes it trivial to show loading states in your UI.