Skip to content

feat: add model lifecycle hooks (onObserve/cleanup)#43

Merged
Moon-DaeSeung merged 1 commit into
mainfrom
feat/lifecycle-hooks
Mar 12, 2026
Merged

feat: add model lifecycle hooks (onObserve/cleanup)#43
Moon-DaeSeung merged 1 commit into
mainfrom
feat/lifecycle-hooks

Conversation

@Moon-DaeSeung
Copy link
Copy Markdown
Contributor

@Moon-DaeSeung Moon-DaeSeung commented Feb 21, 2026

Summary

  • Adds onObserve option to model() that fires when the first subscriber mounts (0→1 transition)
  • The onObserve callback can return a cleanup function that fires when the last subscriber unmounts (1→0 transition)
  • Lifecycle state is tracked per-provider instance via LifecycleState in the registry

Usage

Managing WebSocket Connections

Keep a WebSocket open only while components are subscribed to the model:

import { model } from 'comwit';

const PriceModel = model({
  symbol: 'AAPL',
  price: 0,
  lastUpdated: null as Date | null,
}, {
  onObserve(state) {
    // Opens WebSocket when the first component subscribes
    const ws = new WebSocket(`wss://prices.example.com/${state.symbol}`);
    ws.onmessage = (e) => {
      state.price = JSON.parse(e.data).price;
      state.lastUpdated = new Date();
    };

    // Cleanup: closes when the last component unmounts
    return () => ws.close();
  },
});

Polling Interval

const NotificationModel = model({
  items: [] as Notification[],
  unreadCount: 0,
}, {
  onObserve(state) {
    // Poll every 30s only while the model is observed
    const interval = setInterval(async () => {
      const res = await fetch('/api/notifications');
      const data = await res.json();
      state.items = data.items;
      state.unreadCount = data.unreadCount;
    }, 30_000);

    return () => clearInterval(interval);
  },
});

In Components (unchanged API)

function PriceTicker() {
  // Subscribing via useModel triggers onObserve automatically
  const price = useModel(PriceModel, s => s.price);
  return <span>${price}</span>;
}
// When this component unmounts and no other subscribers remain, cleanup runs

Test plan

  • onObserve fires on first subscription
  • Cleanup fires when all subscribers unmount
  • No firing on subsequent subscriptions (only 0→1)
  • Multiple subscribe/unsubscribe cycles
  • onObserve receives proxy state (mutations work)
  • Per-provider isolation
  • Works with multiple models
  • All 209 existing tests still pass

Closes #39

🤖 Generated with Claude Code

Add onObserve option to model() that fires when the first subscriber
mounts (0→1) and an optional cleanup function when the last subscriber
unmounts (1→0). Lifecycle state is tracked per-provider instance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comwit-docs Ready Ready Preview Feb 21, 2026 1:14pm

@Moon-DaeSeung Moon-DaeSeung merged commit 417ca23 into main Mar 12, 2026
1 check passed
@Moon-DaeSeung Moon-DaeSeung deleted the feat/lifecycle-hooks branch May 19, 2026 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: Model lifecycle hooks (onObserve / onUnobserve)

1 participant