Pages API ๐ โ
Pages in SigPro are special components designed for route-based navigation with automatic cleanup. When you navigate away from a page, all signals, effects, and event listeners created within that page are automatically cleaned up - no memory leaks, no manual cleanup needed.
$.page(setupFunction) โ
Creates a page with automatic cleanup of all signals and effects when navigated away.
import { $, html } from 'sigpro';
export default $.page(() => {
// All signals and effects created here
// will be automatically cleaned up on navigation
const count = $(0);
$.effect(() => {
console.log(`Count: ${count()}`);
});
return html`
<div>
<h1>My Page</h1>
<p>Count: ${count}</p>
<button @click=${() => count(c => c + 1)}>+</button>
</div>
`;
});๐ API Reference โ
| Parameter | Type | Description |
|---|---|---|
setupFunction | Function | Function that returns the page content. Receives context object with params and onUnmount |
Context Object Properties โ
| Property | Type | Description |
|---|---|---|
params | Object | Route parameters passed to the page |
onUnmount | Function | Register cleanup callbacks (alternative to automatic cleanup) |
๐ฏ Basic Usage โ
Simple Page โ
// pages/home.js
import { $, html } from 'sigpro';
export default $.page(() => {
const title = $('Welcome to SigPro');
return html`
<div class="home-page">
<h1>${title}</h1>
<p>This page will clean itself up when you navigate away.</p>
</div>
`;
});Page with Route Parameters โ
// pages/user.js
import { $, html } from 'sigpro';
export default $.page(({ params }) => {
// Access route parameters
const userId = params.id;
const userData = $(null);
const loading = $(false);
// Auto-cleaned effect
$.effect(() => {
loading(true);
$.fetch(`/api/users/${userId}`, null, loading)
.then(data => userData(data));
});
return html`
<div>
${() => loading() ? html`
<div class="spinner">Loading...</div>
` : html`
<h1>User Profile: ${userData()?.name}</h1>
<p>Email: ${userData()?.email}</p>
`}
</div>
`;
});๐งน Automatic Cleanup โ
The magic of $.page is automatic cleanup. Everything created inside the page is tracked and cleaned up:
export default $.page(() => {
// โ
Signals are auto-cleaned
const count = $(0);
const user = $(null);
// โ
Effects are auto-cleaned
$.effect(() => {
document.title = `Count: ${count()}`;
});
// โ
Event listeners are auto-cleaned
window.addEventListener('resize', handleResize);
// โ
Intervals and timeouts are auto-cleaned
const interval = setInterval(() => {
refreshData();
}, 5000);
return html`<div>Page content</div>`;
});
// When navigating away: all signals, effects, listeners, intervals STOP๐ Manual Cleanup with onUnmount โ
Sometimes you need custom cleanup logic. Use onUnmount for that:
export default $.page(({ onUnmount }) => {
// WebSocket connection
const socket = new WebSocket('wss://api.example.com');
socket.onmessage = (event) => {
updateData(JSON.parse(event.data));
};
// Manual cleanup
onUnmount(() => {
socket.close();
console.log('WebSocket closed');
});
return html`<div>Real-time updates</div>`;
});๐ Integration with Router โ
Pages are designed to work seamlessly with $.router:
import { $, html } from 'sigpro';
import HomePage from './pages/Home.js';
import UserPage from './pages/User.js';
import SettingsPage from './pages/Settings.js';
const routes = [
{ path: '/', component: HomePage },
{ path: '/user/:id', component: UserPage },
{ path: '/settings', component: SettingsPage },
];
// Mount router
document.body.appendChild($.router(routes));๐ก Practical Examples โ
Example 1: Data Fetching Page โ
// pages/posts.js
export default $.page(({ params }) => {
const posts = $([]);
const loading = $(true);
const error = $(null);
$.effect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
posts(data);
loading(false);
})
.catch(err => {
error(err.message);
loading(false);
});
});
return html`
<div class="posts-page">
<h1>Blog Posts</h1>
${() => loading() ? html`
<div class="loading">Loading posts...</div>
` : error() ? html`
<div class="error">Error: ${error()}</div>
` : html`
<div class="posts-grid">
${posts().map(post => html`
<article class="post-card">
<h2>${post.title}</h2>
<p>${post.excerpt}</p>
<a href="#/post/${post.id}">Read more</a>
</article>
`)}
</div>
`}
</div>
`;
});Example 2: Real-time Dashboard โ
// pages/dashboard.js
export default $.page(({ onUnmount }) => {
const metrics = $({
cpu: 0,
memory: 0,
requests: 0
});
// Auto-refresh data
const refreshInterval = setInterval(async () => {
const data = await $.fetch('/api/metrics');
if (data) metrics(data);
}, 5000);
// Manual cleanup for interval
onUnmount(() => clearInterval(refreshInterval));
// Live clock
const currentTime = $(new Date());
const clockInterval = setInterval(() => {
currentTime(new Date());
}, 1000);
onUnmount(() => clearInterval(clockInterval));
return html`
<div class="dashboard">
<h1>System Dashboard</h1>
<div class="time">
Last updated: ${() => currentTime().toLocaleTimeString()}
</div>
<div class="metrics-grid">
<div class="metric-card">
<h3>CPU Usage</h3>
<p class="metric-value">${() => metrics().cpu}%</p>
</div>
<div class="metric-card">
<h3>Memory Usage</h3>
<p class="metric-value">${() => metrics().memory}%</p>
</div>
<div class="metric-card">
<h3>Requests/min</h3>
<p class="metric-value">${() => metrics().requests}</p>
</div>
</div>
</div>
`;
});Example 3: Multi-step Form โ
// pages/checkout.js
export default $.page(({ onUnmount }) => {
const step = $(1);
const formData = $({
email: '',
address: '',
payment: ''
});
// Warn user before leaving
const handleBeforeUnload = (e) => {
if (step() < 3) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
onUnmount(() => {
window.removeEventListener('beforeunload', handleBeforeUnload);
});
const nextStep = () => step(s => Math.min(s + 1, 3));
const prevStep = () => step(s => Math.max(s - 1, 1));
return html`
<div class="checkout">
<h1>Checkout - Step ${step} of 3</h1>
${() => {
switch(step()) {
case 1:
return html`
<div class="step">
<h2>Email</h2>
<input
type="email"
:value=${() => formData().email}
@input=${(e) => formData({...formData(), email: e.target.value})}
/>
</div>
`;
case 2:
return html`
<div class="step">
<h2>Address</h2>
<textarea
:value=${() => formData().address}
@input=${(e) => formData({...formData(), address: e.target.value})}
></textarea>
</div>
`;
case 3:
return html`
<div class="step">
<h2>Payment</h2>
<input
type="text"
placeholder="Card number"
:value=${() => formData().payment}
@input=${(e) => formData({...formData(), payment: e.target.value})}
/>
</div>
`;
}
}}
<div class="buttons">
${() => step() > 1 ? html`
<button @click=${prevStep}>Previous</button>
` : ''}
${() => step() < 3 ? html`
<button @click=${nextStep}>Next</button>
` : html`
<button @click=${submitOrder}>Place Order</button>
`}
</div>
</div>
`;
});Example 4: Page with Tabs โ
// pages/profile.js
export default $.page(({ params }) => {
const activeTab = $('overview');
const userData = $(null);
// Load user data
$.effect(() => {
$.fetch(`/api/users/${params.id}`)
.then(data => userData(data));
});
const tabs = {
overview: () => html`
<div>
<h3>Overview</h3>
<p>Username: ${userData()?.username}</p>
<p>Member since: ${userData()?.joined}</p>
</div>
`,
posts: () => html`
<div>
<h3>Posts</h3>
${userData()?.posts.map(post => html`
<div class="post">${post.title}</div>
`)}
</div>
`,
settings: () => html`
<div>
<h3>Settings</h3>
<label>
<input type="checkbox" :checked=${userData()?.emailNotifications} />
Email notifications
</label>
</div>
`
};
return html`
<div class="profile-page">
<h1>${() => userData()?.name}</h1>
<div class="tabs">
${Object.keys(tabs).map(tab => html`
<button
class:active=${() => activeTab() === tab}
@click=${() => activeTab(tab)}
>
${tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
`)}
</div>
<div class="tab-content">
${() => tabs[activeTab()]()}
</div>
</div>
`;
});๐ฏ Advanced Patterns โ
Page with Nested Routes โ
// pages/settings/index.js
export default $.page(({ params }) => {
const section = params.section || 'general';
const sections = {
general: () => import('./general.js').then(m => m.default),
security: () => import('./security.js').then(m => m.default),
notifications: () => import('./notifications.js').then(m => m.default)
};
const currentSection = $(null);
$.effect(() => {
sections[section]().then(comp => currentSection(comp));
});
return html`
<div class="settings">
<nav>
<a href="#/settings/general">General</a>
<a href="#/settings/security">Security</a>
<a href="#/settings/notifications">Notifications</a>
</nav>
<div class="content">
${currentSection}
</div>
</div>
`;
});Page with Authentication โ
// pages/dashboard.js
export default $.page(({ onUnmount }) => {
const isAuthenticated = $(false);
const authCheck = $.effect(() => {
const token = localStorage.getItem('token');
isAuthenticated(!!token);
});
// Redirect if not authenticated
$.effect(() => {
if (!isAuthenticated()) {
$.router.go('/login');
}
});
return html`
<div class="dashboard">
<h1>Protected Dashboard</h1>
<!-- Protected content -->
</div>
`;
});๐ Summary โ
| Feature | Description |
|---|---|
| Automatic Cleanup | All signals, effects, and resources auto-cleaned on navigation |
| Memory Safe | No memory leaks, even with complex nested effects |
| Router Integration | Designed to work perfectly with $.router |
| Parameters | Access route parameters via params object |
| Manual Cleanup | onUnmount for custom cleanup needs |
| Zero Configuration | Just wrap your page in $.page() and it works |
Pro Tip: Always wrap route-based views in
$.page()to ensure proper cleanup. This prevents memory leaks and ensures your app stays performant even after many navigation changes.