# Pages API 📄 | SigPro
Minimalist Reactive Library

Skip to content

# 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.

javascript

    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 ​

javascript

    // 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 ​

javascript

    // 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:

javascript

    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:

javascript

    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`:

javascript

    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 ​

javascript

    // 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 ​

javascript

    // 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 ​

javascript

    // 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 ​

javascript

    // 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 ​

javascript

    // 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 ​

javascript

    // 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.