Update docs
This commit is contained in:
998
docs/api/fetch.md
Normal file
998
docs/api/fetch.md
Normal file
@@ -0,0 +1,998 @@
|
||||
# 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<Object\|null>` | 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`
|
||||
<div>
|
||||
${() => loading() ? html`
|
||||
<div class="spinner">Loading...</div>
|
||||
` : user() ? html`
|
||||
<div>
|
||||
<h2>${user().name}</h2>
|
||||
<p>Email: ${user().email}</p>
|
||||
</div>
|
||||
` : html`
|
||||
<p>No user found</p>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
```
|
||||
|
||||
### 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`
|
||||
<div class="profile">
|
||||
<div class="user-selector">
|
||||
<button @click=${() => userId(1)}>User 1</button>
|
||||
<button @click=${() => userId(2)}>User 2</button>
|
||||
<button @click=${() => userId(3)}>User 3</button>
|
||||
</div>
|
||||
|
||||
${() => {
|
||||
if (loading()) {
|
||||
return html`<div class="spinner">Loading profile...</div>`;
|
||||
}
|
||||
|
||||
if (error()) {
|
||||
return html`<div class="error">${error()}</div>`;
|
||||
}
|
||||
|
||||
if (user()) {
|
||||
return html`
|
||||
<div class="user-info">
|
||||
<h2>${user().name}</h2>
|
||||
<p>Email: ${user().email}</p>
|
||||
<p>Role: ${user().role}</p>
|
||||
<p>Joined: ${new Date(user().joined).toLocaleDateString()}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`<p>Select a user</p>`;
|
||||
}}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### 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`
|
||||
<div class="todo-app">
|
||||
<h1>Todo List</h1>
|
||||
|
||||
<div class="add-todo">
|
||||
<input
|
||||
type="text"
|
||||
:value=${newTodo}
|
||||
@keydown.enter=${addTodo}
|
||||
placeholder="Add a new todo..."
|
||||
/>
|
||||
<button @click=${addTodo}>Add</button>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<button
|
||||
class:active=${() => filter() === 'all'}
|
||||
@click=${() => filter('all')}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
class:active=${() => filter() === 'active'}
|
||||
@click=${() => filter('active')}
|
||||
>
|
||||
Active
|
||||
</button>
|
||||
<button
|
||||
class:active=${() => filter() === 'completed'}
|
||||
@click=${() => filter('completed')}
|
||||
>
|
||||
Completed
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${() => loading() ? html`
|
||||
<div class="spinner">Loading todos...</div>
|
||||
) : html`
|
||||
<ul class="todo-list">
|
||||
${filteredTodos().map(todo => html`
|
||||
<li class="todo-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked=${todo.completed}
|
||||
@change=${() => toggleTodo(todo.id, todo.completed)}
|
||||
/>
|
||||
<span class:completed=${todo.completed}>${todo.text}</span>
|
||||
<button @click=${() => deleteTodo(todo.id)}>🗑️</button>
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### 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`
|
||||
<div class="infinite-scroll">
|
||||
<h1>Posts</h1>
|
||||
|
||||
<div class="posts">
|
||||
${posts().map(post => html`
|
||||
<article class="post">
|
||||
<h2>${post.title}</h2>
|
||||
<p>${post.body}</p>
|
||||
<small>By ${post.author}</small>
|
||||
</article>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<div id="sentinel" class="sentinel">
|
||||
${() => {
|
||||
if (loading()) {
|
||||
return html`<div class="spinner">Loading more...</div>`;
|
||||
}
|
||||
if (error()) {
|
||||
return html`<div class="error">${error()}</div>`;
|
||||
}
|
||||
if (!hasMore()) {
|
||||
return html`<div class="end">No more posts</div>`;
|
||||
}
|
||||
return '';
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### 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`
|
||||
<div class="search">
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="search"
|
||||
:value=${query}
|
||||
placeholder="Search..."
|
||||
class="search-input"
|
||||
/>
|
||||
${() => loading() ? html`
|
||||
<span class="spinner-small">⌛</span>
|
||||
) : ''}
|
||||
</div>
|
||||
|
||||
${() => {
|
||||
if (error()) {
|
||||
return html`<div class="error">${error()}</div>`;
|
||||
}
|
||||
|
||||
if (results().length > 0) {
|
||||
return html`
|
||||
<ul class="results">
|
||||
${results().map(item => html`
|
||||
<li class="result-item">
|
||||
<h3>${item.title}</h3>
|
||||
<p>${item.description}</p>
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
if (query().length >= 2 && !loading()) {
|
||||
return html`<p class="no-results">No results found</p>`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### 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`
|
||||
<form class="contact-form" @submit=${handleSubmit}>
|
||||
<h2>Contact Us</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
:value=${() => formData().name}
|
||||
@input=${(e) => updateField('name', e.target.value)}
|
||||
required
|
||||
?disabled=${submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email:</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
:value=${() => formData().email}
|
||||
@input=${(e) => updateField('email', e.target.value)}
|
||||
required
|
||||
?disabled=${submitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message">Message:</label>
|
||||
<textarea
|
||||
id="message"
|
||||
:value=${() => formData().message}
|
||||
@input=${(e) => updateField('message', e.target.value)}
|
||||
required
|
||||
rows="5"
|
||||
?disabled=${submitting}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
${() => {
|
||||
if (submitting()) {
|
||||
return html`<div class="submitting">Sending...</div>`;
|
||||
}
|
||||
|
||||
if (submitError()) {
|
||||
return html`<div class="error">${submitError()}</div>`;
|
||||
}
|
||||
|
||||
if (submitSuccess()) {
|
||||
return html`<div class="success">Message sent successfully!</div>`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
?disabled=${submitting}
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### 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`
|
||||
<div class="dashboard">
|
||||
<header>
|
||||
<h1>System Dashboard</h1>
|
||||
<div class="refresh-control">
|
||||
<label>
|
||||
Refresh interval:
|
||||
<select :value=${refreshInterval} @change=${(e) => refreshInterval(parseInt(e.target.value))}>
|
||||
<option value="2000">2 seconds</option>
|
||||
<option value="5000">5 seconds</option>
|
||||
<option value="10000">10 seconds</option>
|
||||
<option value="30000">30 seconds</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<!-- Metrics Panel -->
|
||||
<div class="panel metrics">
|
||||
<h2>System Metrics</h2>
|
||||
${() => loading().metrics ? html`
|
||||
<div class="spinner">Loading metrics...</div>
|
||||
) : html`
|
||||
<div class="metrics-grid">
|
||||
<div class="metric">
|
||||
<label>CPU</label>
|
||||
<span>${metrics().cpu || 0}%</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<label>Memory</label>
|
||||
<span>${metrics().memory || 0}%</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<label>Requests</label>
|
||||
<span>${metrics().requests || 0}/s</span>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Alerts Panel -->
|
||||
<div class="panel alerts">
|
||||
<h2>Active Alerts</h2>
|
||||
${() => loading().alerts ? html`
|
||||
<div class="spinner">Loading alerts...</div>
|
||||
) : alerts().length > 0 ? html`
|
||||
<ul>
|
||||
${alerts().map(alert => html`
|
||||
<li class="alert ${alert.severity}">
|
||||
<strong>${alert.type}</strong>
|
||||
<p>${alert.message}</p>
|
||||
<small>${new Date(alert.timestamp).toLocaleTimeString()}</small>
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
) : html`
|
||||
<p class="no-data">No active alerts</p>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Logs Panel -->
|
||||
<div class="panel logs">
|
||||
<h2>Recent Logs</h2>
|
||||
${() => loading().logs ? html`
|
||||
<div class="spinner">Loading logs...</div>
|
||||
) : html`
|
||||
<ul>
|
||||
${logs().map(log => html`
|
||||
<li class="log ${log.level}">
|
||||
<span class="timestamp">${new Date(log.timestamp).toLocaleTimeString()}</span>
|
||||
<span class="message">${log.message}</span>
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### 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`
|
||||
<div class="file-uploader">
|
||||
<h2>Upload Files</h2>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
@change=${handleFileSelect}
|
||||
?disabled=${uploading}
|
||||
/>
|
||||
|
||||
${() => files().length > 0 ? html`
|
||||
<div class="file-list">
|
||||
<h3>Selected Files:</h3>
|
||||
<ul>
|
||||
${files().map(file => html`
|
||||
<li>
|
||||
${file.name} (${(file.size / 1024).toFixed(2)} KB)
|
||||
${() => uploadProgress()[file.name] ? html`
|
||||
<progress value="${uploadProgress()[file.name]}" max="100"></progress>
|
||||
) : ''}
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
|
||||
<button
|
||||
@click=${uploadFiles}
|
||||
?disabled=${uploading}
|
||||
>
|
||||
${() => uploading() ? 'Uploading...' : 'Upload Files'}
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${() => uploadResults().length > 0 ? html`
|
||||
<div class="upload-results">
|
||||
<h3>Upload Results:</h3>
|
||||
<ul>
|
||||
${uploadResults().map(result => html`
|
||||
<li class="${result.success ? 'success' : 'error'}">
|
||||
${result.file}:
|
||||
${result.success ? 'Uploaded successfully' : `Failed: ${result.error}`}
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### 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`
|
||||
<div>
|
||||
${() => loading() ? '<Spinner />' :
|
||||
data() ? '<Data />' :
|
||||
'<Empty />'}
|
||||
</div>
|
||||
`;
|
||||
```
|
||||
|
||||
### 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.
|
||||
Reference in New Issue
Block a user